PONY λ M2 Modula-2

Pascal.CodeCompared.To/Rust

An interactive executable cheatsheet comparing Pascal and Rust

Free Pascal 3.2.2 Rust 1.95
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. Unlike Pascal's writeln, it can accept any type implementing Display — no format-string types like %s or %d. Rust uses {} as the universal placeholder.
Compile & run
{ Single file: fpc hello.pas && ./hello With optimisation: 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 // optimised, with LLVM full-opt // 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. No pkg-config, no vendoring.
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 — string formatting is built into the language macro system. Identifiers can appear inline: {name} is equivalent to {0} with the name variable.
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 optimisations.
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 u8u128 (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 does not have Pascal's Extended (80-bit); use f64 for maximum precision.
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 forces f64 (otherwise ambiguous) 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 numeric literals that could be multiple types.
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 (outside functions), unlike Pascal's constants which also live at module scope. 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 without allocating extra memory.
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 out-of-bounds in debug builds and is checked in release builds too (unless you use get for safe access).
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 live on the stack. 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. Set operations use intersection, union, and difference — iterator methods, not operators.
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 with zero runtime overhead, unlike a garbage collector.
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: value moved // 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, stack-allocated by default. 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 behaviour.
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. Unlike Pascal's nested functions, closures capture variables from their enclosing scope — factor is accessible inside the closure. Closures can be stored in variables, passed to functions, and returned from functions.
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 mapfilter_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 monomorphises 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: free 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 behaviour.
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.