PONYλM2Modula-2

Pascal.CodeCompared.To/Rust

An interactive executable cheatsheet comparing Pascal and Rust

Free Pascal 3.2.2 Rust 1.99.0
Hello World & Building
Hello, World
program HelloWorld; begin writeln('Hello, World!'); end.
fn main() { println!("Hello, World!"); }
println! is a macro (note the !), not a function. Where Pascal's writeln knows only the built-in types, it prints any type implementing Display, including your own. Rust uses {} as the universal placeholder.
Compile & run
{ Single file: fpc hello.pas && ./hello With optimization: fpc -O2 hello.pas With units: fpc -Fu/path/to/units hello.pas }
// Single file (uncommon): // rustc hello.rs && ./hello // Standard workflow with Cargo: // cargo new greet // cd greet // cargo run // compile + execute in one step // cargo build // compile only // cargo build --release // optimized build // cargo test // run all tests
Cargo is Rust's integrated build system and package manager — like Free Pascal's FPC combined with a unit library registry. Cargo.toml declares dependencies; cargo build downloads and compiles them automatically from crates.io, with no manual downloads.
Print with formatting
program PrintFormat; uses SysUtils; var name: string; age: integer; pi: double; begin name := 'Alice'; age := 30; pi := 3.14159; writeln(Format('Name: %s, Age: %d, Pi: %.2f', [name, age, pi])); end.
let name = "Alice"; let age = 30; let pi = 3.14159_f64; println!("Name: {name}, Age: {age}, Pi: {pi:.2}");
Rust uses {variable_name} or {:.2} format specifiers directly in the format string. No uses SysUtils required — the formatting macros are always available. {name} captures the variable name directly, as if it had been passed as an argument.
Variables & Types
Immutable vs mutable bindings
program Bindings; var counter: integer; name: string; { Pascal vars are always mutable } begin counter := 10; name := 'Alice'; counter := counter + 1; { mutation: fine } writeln(counter, ' ', name); end.
let counter = 10; // immutable by default let name = "Alice"; // can't reassign let mut total = 0; // mut required for mutation total += 1; println!("{counter} {name} {total}");
Rust bindings are immutable by default — let without mut is a read-only binding. This is the opposite of Pascal, where every var is mutable. Immutability by default helps the compiler eliminate data races and often enables optimizations.
Integer types
program IntTypes; var small: shortint; { 8-bit signed } medium: integer; { 32-bit signed } large: int64; { 64-bit signed } unsigned: cardinal; { 32-bit unsigned } size: sizeint; { pointer-width signed } begin small := 42; medium := 1000000; large := 9000000000000; unsigned := 4000000000; size := 512; writeln(small, ' ', medium, ' ', large, ' ', unsigned, ' ', size); end.
let small: i8 = 42; let medium: i32 = 1_000_000; let large: i64 = 9_000_000_000_000; let unsigned: u32 = 4_000_000_000; let size: usize = 512; // pointer-width, used for indices and lengths println!("{small} {medium} {large} {unsigned} {size}");
Rust's integer types spell out the size and signedness explicitly: i8, i16, i32, i64, i128 (signed), and u8–u128 (unsigned). isize/usize match the pointer width. The default inferred integer type is i32. Rust allows underscores in literals (1_000_000) for readability; Free Pascal 3.2.2 does not, so the Pascal side writes the digits unbroken.
Floating-point types
program FloatTypes; var single_prec: single; { 32-bit float } double_prec: double; { 64-bit float } begin single_prec := 3.14; double_prec := 3.14159265358979; writeln(single_prec:10:6); writeln(double_prec:20:15); end.
let single_prec: f32 = 3.14; let double_prec: f64 = 3.14159265358979; println!("{single_prec:.6}"); println!("{double_prec:.15}");
Rust has f32 and f64 — matching Pascal's Single and Double. The default inferred float type is f64. Rust has no 80-bit Extended, and on stable Rust f64 is the widest float — f128 exists only behind a nightly feature.
Boolean type
program BoolTypes; var flag: boolean; is_ready: boolean; begin flag := true; is_ready := (3 > 2) and not flag; if is_ready then writeln('Ready') else writeln('Not ready'); end.
let flag: bool = true; let is_ready = (3 > 2) && !flag; if is_ready { println!("Ready"); } else { println!("Not ready"); }
Rust uses &&, ||, and ! for boolean operators — the same as C, unlike Pascal's and, or, and not. Rust's bool is a true 1-byte type; unlike C, Rust's if requires a bool — an integer is never truthy.
Type inference
program TypeInference; var count: integer; { Pascal requires explicit type } message: string; ratio: double; begin count := 42; message := 'hello'; ratio := 3.14; writeln(count, ' ', message, ' ', ratio:5:2); end.
let count = 42; // inferred: i32 let message = "hello"; // inferred: &str let ratio = 3.14_f64; // suffix spells out f64, also the default println!("{count} {message} {ratio:.2}");
Rust's type inference is much more powerful than Free Pascal's: types are inferred from context across the entire function, including from how a variable is used later. Explicit type annotations are needed only when inference is ambiguous — usually for collect(), parse() or an empty collection. An unsuffixed literal is not ambiguous: it defaults to i32 or f64.
Constants
program Constants; const MAX_SIZE = 100; PI = 3.14159; APP_NAME = 'MyApp'; begin writeln('Max: ', MAX_SIZE); writeln('Pi: ', PI:7:5); writeln('App: ', APP_NAME); end.
const MAX_SIZE: usize = 100; const PI: f64 = 3.14159; const APP_NAME: &str = "MyApp"; fn main() { println!("Max: {MAX_SIZE}"); println!("Pi: {PI:.5}"); println!("App: {APP_NAME}"); }
Rust constants require an explicit type annotation and are evaluated at compile time. They can be defined at module scope, as Pascal's are, or inside a function. Rust also has static for values with a fixed memory address — use const for pure values.
Strings
String types
program StringTypes; var greeting: string; { heap-allocated, mutable } short_str: string[20]; { fixed-capacity shortstring } begin greeting := 'Hello, World!'; short_str := 'Hi'; writeln(greeting); writeln(Length(greeting)); end.
// &str: borrowed string slice — immutable, no heap allocation let greeting: &str = "Hello, World!"; // String: owned, heap-allocated, growable let mut owned: String = String::from("Hello"); owned.push_str(", World!"); println!("{greeting}"); println!("{}", owned.len());
Rust has two string types: &str is a borrowed slice (like a view into bytes) and String is an owned, heap-allocated buffer. Pascal's String corresponds to String; string literals in Rust are &str. Both types are always valid UTF-8.
String concatenation
program StringConcat; var first: string; last: string; full: string; begin first := 'John'; last := 'Doe'; full := first + ' ' + last; writeln(full); end.
let first = "John"; let last = "Doe"; // format! is usually clearest: let full = format!("{first} {last}"); // or push_str for incremental building: let mut built = String::from(first); built.push(' '); built.push_str(last); println!("{full}"); println!("{built}");
The + operator works on String (it consumes the left operand), but format! is clearer and handles any mix of &str and String. For building strings incrementally, push_str appends into the string's own buffer, which only has to grow when it runs out of room — so a loop of appends does not build a new string each time.
String methods
program StringMethods; uses SysUtils; var text: string; upper: string; lower: string; begin text := ' Hello, World! '; upper := UpperCase(text); lower := LowerCase(Trim(text)); writeln(upper); writeln(lower); writeln(Pos('World', text)); writeln(Length(text)); end.
let text = " Hello, World! "; let upper = text.to_uppercase(); let lower = text.trim().to_lowercase(); println!("{upper}"); println!("{lower}"); println!("{}", text.find("World").unwrap_or(0)); println!("{}", text.len());
Rust's string methods mirror Pascal's SysUtils functions but are method calls on the value. find returns Option<usize> (a byte offset, not a character position), reflecting Rust's UTF-8 safety. unwrap_or(0) extracts the value or a default — no crash if the substring is absent.
Split and iterate
{$H+} program StringSplit; uses StrUtils, Types; var csv: string; parts: TStringDynArray; part: string; begin csv := 'alice,bob,carol'; parts := SplitString(csv, ','); for part in parts do writeln(part); end.
let csv = "alice,bob,carol"; for part in csv.split(',') { println!("{part}"); }
Rust's split returns a lazy iterator — no intermediate array is allocated. The iterator yields &str slices pointing into the original string. To collect into an owned Vec<&str>, call .collect() on the iterator.
Collections
Dynamic arrays
{$modeswitch arrayoperators} program DynamicArray; var numbers: array of integer; begin SetLength(numbers, 3); numbers[0] := 10; numbers[1] := 20; numbers[2] := 30; numbers := numbers + [40, 50]; writeln(Length(numbers)); writeln(numbers[0]); end.
let mut numbers: Vec<i32> = Vec::new(); numbers.push(10); numbers.push(20); numbers.push(30); numbers.extend([40, 50]); println!("{}", numbers.len()); println!("{}", numbers[0]);
Vec<T> is Rust's dynamic array — equivalent to Pascal's dynamic array but with a typed API. push appends, len() returns the count. Indexing with [] panics on an out-of-bounds index, in release builds as well as debug ones; get returns an Option instead.
Fixed-size arrays
program FixedArray; var primes: array[1..5] of integer; index: integer; begin primes[1] := 2; primes[2] := 3; primes[3] := 5; primes[4] := 7; primes[5] := 11; for index := 1 to 5 do write(primes[index], ' '); writeln; end.
let primes: [i32; 5] = [2, 3, 5, 7, 11]; for prime in &primes { print!("{prime} "); } println!();
Rust fixed arrays are 0-indexed (unlike Pascal's 1-indexed), and the size is part of the type: [i32; 5] is a distinct type from [i32; 6]. Fixed arrays are stored inline — on the stack when they are local. Iterate with &array to borrow elements rather than move them.
Hash maps
program HashMapExample; uses Generics.Collections; var scores: specialize TDictionary<String, Integer>; begin scores := specialize TDictionary<String, Integer>.Create; try scores.Add('Alice', 95); scores.Add('Bob', 87); writeln('Alice: ', scores['Alice']); writeln('Count: ', scores.Count); finally scores.Free; end; end.
use std::collections::HashMap; fn main() { let mut scores: HashMap<&str, i32> = HashMap::new(); scores.insert("Alice", 95); scores.insert("Bob", 87); println!("Alice: {}", scores["Alice"]); println!("Count: {}", scores.len()); }
HashMap is in the standard library's std::collections module — a single use statement brings it in. No manual Free needed: the map is dropped when it goes out of scope. Accessing a missing key with [] panics; use .get(key) for safe access returning Option<&V>.
Sets
program SetExample; type TFruit = (apple, banana, cherry, date); TFruitSet = set of TFruit; var fruit_set: TFruitSet; your_fruit: TFruitSet; begin fruit_set := [apple, banana, cherry]; your_fruit := [banana, cherry, date]; if apple in fruit_set then writeln('Has apple'); writeln((fruit_set * your_fruit) = [banana, cherry]); { intersection } end.
use std::collections::HashSet; fn main() { let fruit_set: HashSet<&str> = ["apple", "banana", "cherry"].into(); let your_fruit: HashSet<&str> = ["banana", "cherry", "date"].into(); if fruit_set.contains("apple") { println!("Has apple"); } let intersection: HashSet<&&str> = fruit_set.intersection(&your_fruit).collect(); println!("{}", intersection.len() == 2); }
Pascal's built-in set of type is a compact bitset — elegant for small finite domains. Rust's HashSet works on any hashable type and any size, but is heap-allocated. The methods intersection, union and difference return lazy iterators; the operators &, | and - also work on two &HashSet values and build a new set.
Ownership & Memory
Automatic cleanup — no Free needed
program ManualFree; uses Generics.Collections; var numbers: specialize TList<Integer>; begin numbers := specialize TList<Integer>.Create; try numbers.Add(1); numbers.Add(2); writeln(numbers.Count); finally numbers.Free; { MUST free manually } end; end.
fn main() { let mut numbers: Vec<i32> = Vec::new(); numbers.push(1); numbers.push(2); println!("{}", numbers.len()); // numbers is automatically freed here when it goes out of scope }
Rust's ownership system tracks every heap allocation at compile time. When a value's owner goes out of scope, the compiler inserts a drop call automatically — no try/finally/Free pattern required. This is deterministic cleanup at a known point, with no garbage collector running in the background.
Move semantics
program StringAssign; var original: string; copy: string; begin original := 'hello'; copy := original; { Pascal copies the string } copy := 'world'; writeln(original); { 'hello' — unaffected } writeln(copy); { 'world' } end.
let original = String::from("hello"); let copy = original; // ownership MOVED — original is now invalid // println!("{original}"); // compile error: borrow of moved value: `original` // To keep both, clone: let first = String::from("hello"); let second = first.clone(); println!("{first}"); // "hello" println!("{second}"); // "hello"
Assigning a String to another variable moves ownership — the original binding becomes unusable. This prevents double-free bugs. To keep a copy, call .clone(). Primitive types like i32 and f64 implement Copy and are always copied, just like Pascal integers.
Borrowing — passing without transferring ownership
program PassByValue; procedure PrintLength(text: string); { Pascal copies the string } begin writeln('Length: ', Length(text)); end; begin PrintLength('Hello'); end.
fn print_length(text: &str) { // borrow, not move println!("Length: {}", text.len()); } fn main() { let greeting = String::from("Hello"); print_length(&greeting); // lend a reference println!("{greeting}"); // still owns it }
The & prefix creates a reference — a borrow of the value without transferring ownership. The function receives a read-only view; the caller retains ownership. &str accepts both &String and string literal &str — it is the general borrowed string type.
Heap allocation — Box vs New
program HeapAlloc; type PNode = ^TNode; TNode = record value: integer; next: PNode; end; var node: PNode; begin New(node); node^.value := 42; node^.next := nil; writeln(node^.value); Dispose(node); { must free } end.
struct Node { value: i32, next: Option<Box<Node>>, } fn main() { let node = Box::new(Node { value: 42, next: None }); println!("{}", node.value); // node is automatically freed here }
Box<T> is the Rust equivalent of a Pascal typed pointer (^T). It puts a value on the heap and owns it — no Dispose needed. Option<Box<Node>> replaces nullable PNode pointers: None is the safe equivalent of nil.
Structs & Methods
Struct definition
program StructBasic; type TPerson = record name: string; age: integer; end; var person: TPerson; begin person.name := 'Alice'; person.age := 30; writeln(person.name, ' is ', person.age); end.
struct Person { name: String, age: i32, } fn main() { let person = Person { name: String::from("Alice"), age: 30 }; println!("{} is {}", person.name, person.age); }
Rust structs and Pascal records are conceptually identical: named fields with types, stored inline. The main syntax differences: Rust uses struct, fields use commas not semicolons, and the type annotation comes after a colon. Rust structs are value types that move on assignment (unless they implement Copy).
Methods on structs
program StructMethods; type TRectangle = record width: double; height: double; end; function RectArea(const rect: TRectangle): double; begin Result := rect.width * rect.height; end; function RectPerimeter(const rect: TRectangle): double; begin Result := 2 * (rect.width + rect.height); end; var rect: TRectangle; begin rect.width := 5.0; rect.height := 3.0; writeln(RectArea(rect):6:2); writeln(RectPerimeter(rect):6:2); end.
struct Rectangle { width: f64, height: f64, } impl Rectangle { fn area(&self) -> f64 { self.width * self.height } fn perimeter(&self) -> f64 { 2.0 * (self.width + self.height) } } fn main() { let rect = Rectangle { width: 5.0, height: 3.0 }; println!("{:.2}", rect.area()); println!("{:.2}", rect.perimeter()); }
Rust's impl block attaches methods to a struct. &self is a borrowed reference to the instance — the Rust equivalent of Pascal's implicit first parameter in object procedures. Methods are called with dot notation: rect.area(). The data definition and methods are separate, allowing methods to be added in multiple impl blocks.
Constructor pattern
program ConstructorPattern; type TPoint = record x, y: double; end; function NewPoint(x_val, y_val: double): TPoint; begin Result.x := x_val; Result.y := y_val; end; var point: TPoint; begin point := NewPoint(3.0, 4.0); writeln(point.x:4:1, ' ', point.y:4:1); end.
struct Point { x: f64, y: f64, } impl Point { fn new(x: f64, y: f64) -> Self { Point { x, y } } fn distance_from_origin(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() } } fn main() { let point = Point::new(3.0, 4.0); println!("{:.1} {:.1}", point.x, point.y); println!("{:.2}", point.distance_from_origin()); }
The convention in Rust is an associated function named new (not a special language keyword). Self is an alias for the struct type inside impl. The shorthand Point { x, y } avoids repeating the field name when the variable name matches — called field init shorthand.
Enums & Pattern Matching
Enumerations
program EnumBasic; type TDirection = (North, South, East, West); var direction: TDirection; begin direction := North; case direction of North: writeln('Going north'); South: writeln('Going south'); East: writeln('Going east'); West: writeln('Going west'); end; end.
#[derive(Debug)] enum Direction { North, South, East, West } fn main() { let direction = Direction::North; match direction { Direction::North => println!("Going north"), Direction::South => println!("Going south"), Direction::East => println!("Going east"), Direction::West => println!("Going west"), } }
Rust's match must be exhaustive — if you forget a variant, it's a compile error. The #[derive(Debug)] attribute auto-generates a debug formatter so you can print enum values with {:?}. Variants are namespaced: Direction::North, not bare North.
Enums with data — variant records vs tagged enums
program VariantRecord; type TShapeKind = (shCircle, shRectangle); TShape = record case kind: TShapeKind of shCircle: (radius: double); shRectangle: (width, height: double); end; var shape: TShape; begin shape.kind := shCircle; shape.radius := 5.0; if shape.kind = shCircle then writeln('Area: ', Pi * shape.radius * shape.radius:8:3); end.
use std::f64::consts::PI; enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, } fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => PI * radius * radius, Shape::Rectangle { width, height } => width * height, } } fn main() { let shape = Shape::Circle { radius: 5.0 }; println!("Area: {:.3}", area(&shape)); }
Rust's enum variants can hold data — this is the modern, type-safe replacement for Pascal's variant records. The key difference: the compiler ensures you only access radius when the variant is Circle. In Pascal, nothing prevents reading shape.width when kind is shCircle — undefined behavior.
Pattern matching with guards
program PatternMatch; var score: integer; begin score := 85; case score of 90..100: writeln('A'); 80..89: writeln('B'); 70..79: writeln('C'); 0..69: writeln('F'); else writeln('Invalid'); end; end.
let score = 85; let grade = match score { 90..=100 => "A", 80..=89 => "B", 70..=79 => "C", 0..=69 => "F", _ => "Invalid", }; println!("{grade}");
Rust's match uses inclusive range patterns 90..=100 (note the =), matching Pascal's 90..100. The _ wildcard is the Rust equivalent of Pascal's else in a case statement. match is an expression that returns a value — no need for a separate variable and assignment.
Error Handling
Absence — Option instead of nil
program NilCheck; type PInteger = ^integer; var maybe_value: PInteger; number: integer; begin maybe_value := nil; if maybe_value <> nil then begin number := maybe_value^; writeln('Value: ', number); end else writeln('No value'); end.
let maybe_value: Option<i32> = None; match maybe_value { Some(number) => println!("Value: {number}"), None => println!("No value"), } // or with if let: if let Some(value) = maybe_value { println!("Got: {value}"); }
Option<T> replaces nullable pointers. None is the safe equivalent of nil — but the compiler forces you to handle it before you can use the value. There is no null dereference in safe Rust. The if let syntax is shorthand for a one-branch match.
Result — typed error returns
program TryParse; uses SysUtils; var text: string; number: integer; error_code: integer; begin text := '42'; Val(text, number, error_code); if error_code = 0 then writeln('Parsed: ', number) else writeln('Parse error at position ', error_code); end.
let text = "42"; match text.parse::<i32>() { Ok(number) => println!("Parsed: {number}"), Err(error) => println!("Parse error: {error}"), }
Result<T, E> is the Rust replacement for out-parameters and error codes. Ok(value) carries a success value; Err(error) carries the error. The compiler forces you to handle both cases — forgetting to check is a compile warning, not a silent bug. This is Rust's primary error-handling mechanism.
Exceptions vs Result propagation
program ExceptionHandling; uses SysUtils; function DivideNumbers(a, b: double): double; begin if b = 0 then raise Exception.Create('Division by zero'); Result := a / b; end; begin try writeln(DivideNumbers(10, 2):6:3); writeln(DivideNumbers(10, 0):6:3); except on error: Exception do writeln('Error: ', error.Message); end; end.
fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { return Err(String::from("Division by zero")); } Ok(a / b) } fn main() { match divide(10.0, 2.0) { Ok(result) => println!("{result:.3}"), Err(error) => println!("Error: {error}"), } match divide(10.0, 0.0) { Ok(result) => println!("{result:.3}"), Err(error) => println!("Error: {error}"), } }
Rust has no exceptions. Errors are returned as Result values — they are explicit in every function signature. This makes error paths visible and prevents silent exception propagation. For truly unrecoverable errors (bugs, not expected failures), Rust has panic!, which unwinds the thread.
The ? operator — propagate errors up
{ Pascal has no direct equivalent — you must handle each error explicitly or let exceptions propagate automatically: function ReadAndParse(filename: string): integer; var file_handle: TextFile; line: string; value, error_code: integer; begin AssignFile(file_handle, filename); Reset(file_handle); { raises EInOutError if file missing } ReadLn(file_handle, line); CloseFile(file_handle); Val(line, value, error_code); if error_code <> 0 then raise EConvertError.Create('Bad number'); Result := value; end; }
use std::num::ParseIntError; use std::fs; fn read_and_parse(filename: &str) -> Result<i32, Box<dyn std::error::Error>> { let contents = fs::read_to_string(filename)?; // ? propagates Err let number = contents.trim().parse::<i32>()?; // ? propagates Err Ok(number) } fn main() { match read_and_parse("numbers.txt") { Ok(number) => println!("Got: {number}"), Err(error) => println!("Failed: {error}"), } }
The ? operator short-circuits a function on Err: it either unwraps Ok(value) into value, or returns Err(e) immediately. This gives explicit, composable error propagation without exception machinery. The pattern reads as "try each step; bail out if any fails."
Closures & Iterators
Closures — inline anonymous functions
program Closures; { Pascal has no closures; the closest is a nested function (can't capture vars) } function Double(n: integer): integer; begin Result := n * 2; end; var numbers: array of integer; i: integer; begin numbers := [1, 2, 3, 4, 5]; for i := Low(numbers) to High(numbers) do writeln(Double(numbers[i])); end.
let numbers = vec![1, 2, 3, 4, 5]; // closure captures its environment: let factor = 3; let triple = |n: i32| n * factor; for number in &numbers { println!("{}", triple(*number)); }
Rust closures are anonymous functions written with |params| body syntax. Like a Pascal nested function, a closure can read variables from its enclosing scope — factor here — but unlike one it is a value: it can be stored in a variable, passed to a function, and returned from one.
Mapping over a collection
program MapExample; var numbers: array of integer; doubled: array of integer; index: integer; begin numbers := [1, 2, 3, 4, 5]; SetLength(doubled, Length(numbers)); for index := Low(numbers) to High(numbers) do doubled[index] := numbers[index] * 2; for index := Low(doubled) to High(doubled) do write(doubled[index], ' '); writeln; end.
let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect(); for value in &doubled { print!("{value} "); } println!();
Rust iterators are lazy — map produces no values until consumed. .collect() drives the iterator to completion and gathers results into a Vec. The type annotation on doubled tells collect what container to build. This pattern replaces Pascal's explicit index loops.
Filtering a collection
program FilterExample; var numbers: array of integer; evens: array of integer; count: integer; index: integer; begin numbers := [1, 2, 3, 4, 5, 6]; count := 0; for index := Low(numbers) to High(numbers) do if numbers[index] mod 2 = 0 then Inc(count); SetLength(evens, count); count := 0; for index := Low(numbers) to High(numbers) do if numbers[index] mod 2 = 0 then begin evens[count] := numbers[index]; Inc(count); end; for index := Low(evens) to High(evens) do write(evens[index], ' '); writeln; end.
let numbers = vec![1, 2, 3, 4, 5, 6]; let evens: Vec<&i32> = numbers.iter().filter(|n| *n % 2 == 0).collect(); for value in &evens { print!("{value} "); } println!();
filter keeps only the elements for which the closure returns true. Combining filter and map — filter_map, or chaining — replaces most hand-written accumulation loops. Iterator adapters compose: .iter().filter(...).map(...).collect() is a single lazy pipeline.
Folding / reducing
program SumExample; uses Math; var numbers: array of integer; total: integer; index: integer; begin numbers := [1, 2, 3, 4, 5]; total := 0; for index := Low(numbers) to High(numbers) do total := total + numbers[index]; writeln('Sum: ', total); writeln('Max: ', MaxIntValue(numbers)); end.
let numbers = vec![1, 2, 3, 4, 5]; let total: i32 = numbers.iter().sum(); let maximum = numbers.iter().max().unwrap(); println!("Sum: {total}"); println!("Max: {maximum}");
sum() and max() are common terminal operations on iterators. For a custom reduction, use .fold(initial, |accumulator, value| ...). max() returns Option<&i32> because an empty iterator has no maximum — unwrap() assumes it is non-empty.
Traits & Generics
Traits — like Pascal interfaces
program TraitExample; type IDescribable = interface function Describe: string; end; TCar = class(TInterfacedObject, IDescribable) private FMake, FModel: string; public constructor Create(make, model: string); function Describe: string; end; constructor TCar.Create(make, model: string); begin FMake := make; FModel := model; end; function TCar.Describe: string; begin Result := FMake + ' ' + FModel; end; var car: IDescribable; begin car := TCar.Create('Toyota', 'Corolla'); writeln(car.Describe); end.
trait Describable { fn describe(&self) -> String; } struct Car { make: String, model: String } impl Describable for Car { fn describe(&self) -> String { format!("{} {}", self.make, self.model) } } fn print_description(item: &dyn Describable) { println!("{}", item.describe()); } fn main() { let car = Car { make: String::from("Toyota"), model: String::from("Corolla") }; print_description(&car); }
Rust traits are like Pascal/Delphi interfaces — they define a contract that types implement. The key difference: trait implementations are explicit (impl Describable for Car), not declared in the type definition. &dyn Describable is a trait object — a pointer to any type implementing the trait, used for dynamic dispatch.
Generics
{$modeswitch advancedrecords} program GenericExample; type generic TStack<T> = record private items: array of T; public procedure Push(value: T); function Pop: T; function Count: integer; end; procedure TStack.Push(value: T); begin SetLength(items, Length(items) + 1); items[High(items)] := value; end; function TStack.Pop: T; begin Result := items[High(items)]; SetLength(items, Length(items) - 1); end; function TStack.Count: integer; begin Result := Length(items); end; var stack: specialize TStack<integer>; begin stack.Push(1); stack.Push(2); writeln(stack.Count); writeln(stack.Pop); end.
struct Stack<T> { items: Vec<T>, } impl<T> Stack<T> { fn new() -> Self { Stack { items: Vec::new() } } fn push(&mut self, value: T) { self.items.push(value); } fn pop(&mut self) -> Option<T> { self.items.pop() } fn count(&self) -> usize { self.items.len() } } fn main() { let mut stack: Stack<i32> = Stack::new(); stack.push(1); stack.push(2); println!("{}", stack.count()); println!("{:?}", stack.pop()); }
Rust generics use the same <T> syntax as Pascal's generic units. Trait bounds constrain what types are allowed: fn largest<T: PartialOrd>(list: &[T]) -> T restricts T to comparable types. The compiler monomorphizes generics — one compiled version per concrete type used, with zero runtime overhead.
Derive — auto-implement common traits
program DeriveExample; { In Pascal, you implement equality, comparison, and string conversion manually: } uses SysUtils; type TPoint = record x, y: double; end; function PointEquals(a, b: TPoint): boolean; begin Result := (a.x = b.x) and (a.y = b.y); end; function PointToString(p: TPoint): string; begin Result := Format('Point(%g, %g)', [p.x, p.y]); end; var first_point: TPoint; second_point: TPoint; begin first_point.x := 3.0; first_point.y := 4.0; second_point := first_point; { records copy by value } writeln(PointEquals(first_point, second_point)); writeln(PointToString(first_point)); end.
#[derive(Debug, Clone, PartialEq)] struct Point { x: f64, y: f64, } fn main() { let point1 = Point { x: 3.0, y: 4.0 }; let point2 = point1.clone(); // Clone: an explicit copy println!("{:?}", point1); // Debug: prints Point { x: 3.0, y: 4.0 } println!("{}", point1 == point2); // PartialEq: true }
The #[derive(...)] attribute auto-generates trait implementations. Debug enables {:?} printing; Clone enables .clone(); PartialEq enables ==. This replaces the boilerplate Pascal requires for every record type. You can still implement traits manually for custom behavior.
Modules & Crates
Modules — uses units vs mod
program Main; // In Pascal each unit lives in its own file. A MathUtils unit looks like: // // unit MathUtils; // interface // function Square(n: integer): integer; // implementation // function Square(n: integer): integer; // begin // Result := n * n; // end; // end. // // Another file pulls it in with a "uses MathUtils;" clause. Inlined here to run: function Square(n: integer): integer; begin Result := n * n; end; begin writeln(Square(5)); end.
// In a single file: mod math_utils { pub fn square(n: i32) -> i32 { n * n } } fn main() { println!("{}", math_utils::square(5)); }
Rust modules can be inline (inside a file) or in separate files. A module in its own file is at src/math_utils.rs and declared with mod math_utils; in src/main.rs. The pub keyword controls visibility — like Pascal's interface section. Names not marked pub are private to the module.
Importing names
program ImportExample; uses SysUtils, { Format, IntToStr, etc. } Generics.Collections; { TDictionary, TList, etc. } var numbers: specialize TList<Integer>; begin numbers := specialize TList<Integer>.Create; numbers.Add(1); writeln(IntToStr(numbers.Count)); numbers.Free; end.
use std::collections::HashMap; // bring HashMap into scope use std::fmt::Write; // bring Write trait into scope fn main() { let mut counts: HashMap<&str, i32> = HashMap::new(); counts.insert("apple", 3); let mut output = String::new(); write!(output, "Count: {}", counts["apple"]).unwrap(); println!("{output}"); }
Rust's use statement imports a single name (or glob with ::*), unlike Pascal's uses which imports an entire unit. Many standard types (Vec, String, Option, Result) are in the implicit prelude and need no use. Explicit imports make dependencies visible and avoid name collisions.

Thank you — anything else?