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 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 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 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 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.