Program Structure & Output
Hello, World
Odin's entry point is a procedure named
main inside package main, rather than Pascal's bare begin…end. block. Note the double colon: :: is Odin's constant-declaration operator, and a procedure is simply a constant whose value is code.program HelloWorld;
begin
writeln('Hello, World!');
end. package main
import "core:fmt"
main :: proc() {
fmt.println("Hello, World!")
} Nothing is in scope implicitly — even printing requires
import "core:fmt", where Pascal's writeln is a compiler built-in.Declaration order
Odin has no declaration sections and no ordering requirement at file scope — constants, types, variables, and procedures may appear in any order. The three forms are told apart by punctuation:
NAME :: value is a compile-time constant, name: Type a variable, name := value an inferred one.program Shape;
const
Limit = 10;
type
Counter = Integer;
var
total: Counter;
procedure Report;
begin
writeln('total is ', total);
end;
begin
total := Limit * 2;
Report;
end. package main
import "core:fmt"
LIMIT :: 10
Counter :: distinct int
total: Counter
report :: proc() {
fmt.println("total is", total)
}
main :: proc() {
total = LIMIT * 2
report()
} Pascal enforces a fixed order —
const, type, var, procedures, main block — and a name must be declared before use. In Odin report could just as well be written after main.Compiling & running
Odin compiles a directory as a single package, so every
.odin file beside this one is in the same namespace with no import. The -file flag asks it to make an exception and compile just one file.{ Compile a single file and run it:
fpc hello.pas && ./hello
With optimization:
fpc -O2 hello.pas
Point the compiler at extra units:
fpc -Fu/path/to/units hello.pas } // Compile and run in one step (single file):
// odin run hello.odin -file
// Compile a whole directory as one package:
// odin build .
// Optimized release build:
// odin build . -o:speed
// Cross-compile — no extra toolchain to install:
// odin build . -target:linux_arm64 Free Pascal compiles one
.pas file at a time and finds its units through -Fu search paths; Odin needs no search paths and no unit list at all.Comments
Odin has
// line comments and /* */ block comments — and the block comments nest, which Pascal's do not.program Comments;
begin
{ Curly-brace comment — the traditional form }
(* Parenthesis-star comment — equivalent *)
// Line comment (a Free Pascal extension)
writeln('done');
end. package main
import "core:fmt"
main :: proc() {
// Line comment
/* Block comment —
and unlike Pascal, these nest:
/* inner block */
so commenting out a region is always safe. */
fmt.println("done")
} Commenting out a region of Pascal that already contains a
{ … } comment terminates the outer comment early at the inner } — a classic source of confusing errors. In Odin the compiler counts opening and closing pairs, so wrapping any region always does what you meant.Formatted output
Odin has one formatting mechanism: a printf-style family in
core:fmt, where %v formats a value of any type — including structs and enums — without you naming its type.program FormatOutput;
uses SysUtils;
var
name : string;
age : Integer;
ratio: Double;
begin
name := 'Alice';
age := 30;
ratio := 3.14159;
writeln(Format('Name: %s, Age: %d', [name, age]));
writeln('Ratio: ', ratio:6:2);
end. package main
import "core:fmt"
main :: proc() {
name := "Alice"
age := 30
ratio := 3.14159
fmt.printfln("Name: %s, Age: %d", name, age)
fmt.printfln("Ratio: %6.2f", ratio)
// %v prints any value using its natural representation
fmt.printfln("all at once: %v %v %v", name, age, ratio)
} Pascal has two unrelated mechanisms: the
writeln width specifiers built into the compiler (ratio:6:2) and Format from SysUtils taking an array of const. Odin folds both into one.Variables & Types
Declaring variables
The
name: Type ordering is the one Pascal chose, colon included. What changes is that declarations are ordinary statements — they live where the variable is first needed, not in a var block at the top.program Variables;
var
count : Integer;
ratio : Double;
flag : Boolean;
letter: Char;
begin
count := 10;
ratio := 2.5;
flag := True;
letter := 'A';
writeln(count, ' ', ratio:4:2, ' ', flag, ' ', letter);
end. package main
import "core:fmt"
main :: proc() {
count: int
ratio: f64
flag: bool
letter: rune
count = 10
ratio = 2.5
flag = true
letter = 'A'
fmt.printfln("%d %.2f %v %v", count, ratio, flag, letter)
} Every variable is zero-initialized, so an uninitialized
int is reliably 0 rather than whatever was on the stack.Type inference
The
:= operator declares a new variable and infers its type from the right-hand side. Plain = is Odin's assignment operator.program Inference;
var
{ Pascal has no inference — every type is spelled out }
count : Integer;
message: string;
begin
count := 42;
message := 'inferred nothing';
writeln(count, ' ', message);
end. package main
import "core:fmt"
main :: proc() {
// := declares and infers in one step
count := 42
message := "inferred from the literal"
// The explicit form is still available when you want a specific width
precise: i64 = 42
fmt.println(count, message, precise)
} The same two characters mean something subtly different in each language: Pascal uses
:= purely for assignment to an already-declared variable, while in Odin it always introduces a new name.Constants
The
:: operator declares a compile-time constant, and like Pascal's const the type is inferred from the value.program Constants;
const
MaxSize = 100;
Pi = 3.14159;
Greeting = 'Hello';
begin
writeln(MaxSize, ' ', Pi:6:4, ' ', Greeting);
end. package main
import "core:fmt"
MAX_SIZE :: 100
PI :: 3.14159
GREETING :: "Hello"
main :: proc() {
fmt.printfln("%d %.4f %s", MAX_SIZE, PI, GREETING)
} Odin constants are untyped until used, so
MAX_SIZE can be assigned to an i8 or an i64 without a cast as long as the value fits — the compiler range-checks at each use site.Numeric types
Odin names every numeric type after its width and signedness —
i8 through i64, u8 through u64, f16/f32/f64. Plain int and uint are pointer-sized.program NumericTypes;
var
small : ShortInt; { 8-bit signed }
medium : SmallInt; { 16-bit signed }
large : LongInt; { 32-bit signed }
huge : Int64; { 64-bit signed }
counter: Cardinal; { 32-bit unsigned }
single1: Single; { 32-bit float }
double1: Double; { 64-bit float }
begin
small := -128; medium := 32767; large := 2147483647;
huge := 9223372036854775807; counter := 4294967295;
single1 := 1.5; double1 := 2.5;
writeln(small, ' ', medium, ' ', large);
writeln(huge, ' ', counter, ' ', single1:4:1, ' ', double1:4:1);
end. package main
import "core:fmt"
main :: proc() {
small: i8 = -128
medium: i16 = 32767
large: i32 = 2147483647
huge: i64 = 9223372036854775807
counter: u32 = 4294967295
single: f32 = 1.5
double: f64 = 2.5
fmt.println(small, medium, large)
fmt.println(huge, counter, single, double)
// Widths are in the names, so there is no ambiguity across platforms
fmt.println("size of int on this target:", size_of(int), "bytes")
} That removes the guesswork behind Pascal names like
Integer, whose size depends on the mode and target. int is the closest analog to Pascal's Integer, and size_of tells you what it is on this target.Type conversion
Odin performs no implicit numeric conversions — not even the widening
Integer-to-Double assignment Pascal allows silently. Writing f64(whole) is mandatory.program Conversion;
uses SysUtils;
var
whole : Integer;
fraction: Double;
text : string;
begin
whole := 7;
fraction := whole; { widening is implicit }
whole := Trunc(fraction);
text := IntToStr(whole);
writeln(fraction:4:1, ' ', text, ' ', StrToInt('42'));
end. package main
import "core:fmt"
import "core:strconv"
main :: proc() {
whole := 7
// Every numeric conversion is explicit, even a widening one
fraction := f64(whole)
back := int(fraction)
buffer: [8]byte
text := strconv.itoa(buffer[:], back)
parsed, ok := strconv.parse_int("42")
fmt.println(fraction, text, parsed, ok)
} The upside is that a mixed-width arithmetic expression can never quietly truncate or change signedness, which is the source of a whole class of bugs in both Pascal and C.
Type aliases vs distinct types
Odin offers both behaviors and makes you choose.
Temperature :: f64 is an alias with Pascal's semantics; Celsius :: distinct f64 creates a type that shares f64's representation and operators but will not implicitly convert to or from it.program DistinctTypes;
type
Celsius = Double;
Fahrenheit = Double;
var
indoors: Celsius;
outdoors: Fahrenheit;
begin
indoors := 21.5;
outdoors := indoors; { compiles — both are just Double }
writeln(outdoors:4:1);
end. package main
import "core:fmt"
// A plain alias — interchangeable with f64
Temperature :: f64
// distinct creates a genuinely separate type
Celsius :: distinct f64
Fahrenheit :: distinct f64
to_fahrenheit :: proc(degrees: Celsius) -> Fahrenheit {
return Fahrenheit(f64(degrees) * 9 / 5 + 32)
}
main :: proc() {
indoors: Celsius = 21.5
// outdoors: Fahrenheit = indoors // rejected at compile time
outdoors := to_fahrenheit(indoors)
fmt.printfln("%.1fC is %.1fF", f64(indoors), f64(outdoors))
} Pascal's
type Celsius = Double creates an alias, not a new type — assigning a Celsius to a Fahrenheit compiles happily, because both are Double as far as the compiler is concerned. distinct is how you make unit-mismatch bugs a compile error rather than a code-review responsibility.Strings
String basics
Two things differ from Free Pascal's
AnsiString: Odin indexes from 0, and a string is UTF-8, so len counts bytes and greeting[0] yields a single byte rather than necessarily a whole character.program StringBasics;
var
greeting: string;
begin
greeting := 'Hello, Pascal!';
writeln(greeting);
writeln('Length: ', Length(greeting));
writeln('First character: ', greeting[1]); { 1-based }
end. package main
import "core:fmt"
main :: proc() {
greeting := "Hello, Odin!"
fmt.println(greeting)
fmt.println("Length in bytes:", len(greeting))
fmt.println("First byte:", rune(greeting[0])) // 0-based
} For text that may contain non-ASCII, iterate with
for character in greeting, which decodes runes — the row below covers that.Strings are immutable
An Odin
string is an immutable view — a pointer and a length — so it can be sliced and passed around without copying, but never written through. Building a modified version allocates, hence the defer delete.program Mutation;
var
greeting: string;
begin
greeting := 'hello';
greeting[1] := 'H'; { legal — strings are mutable buffers }
writeln(greeting);
end. package main
import "core:fmt"
import "core:strings"
main :: proc() {
greeting := "hello"
// greeting[0] = 'H' // rejected: a string is read-only
// Build a new value instead
capitalized := strings.concatenate({"H", greeting[1:]})
defer delete(capitalized)
fmt.println(capitalized)
} Free Pascal strings are reference-counted mutable buffers that copy on write, so poking a character in place is legal there. Odin makes you construct a new string, and makes the allocation visible.
Concatenation
Odin has no
+ operator for strings. strings.concatenate and strings.join return memory you own and must delete.program Concatenation;
var
first, last, full: string;
begin
first := 'Ada';
last := 'Lovelace';
full := first + ' ' + last; { + concatenates }
writeln(full);
writeln(Concat(first, '-', last));
end. package main
import "core:fmt"
import "core:strings"
main :: proc() {
first := "Ada"
last := "Lovelace"
// Every concatenation allocates, so ownership is explicit
full := strings.concatenate({first, " ", last})
defer delete(full)
fmt.println(full)
joined := strings.join({first, last}, "-")
defer delete(joined)
fmt.println(joined)
} This is deliberate: concatenation allocates, and the language refuses to hide an allocation behind an operator. Free Pascal's reference counting handles the cleanup for you, at the cost of you not knowing where the allocations happen.
Substrings & slicing
Slicing uses
low:high with a 0-based, exclusive upper bound, and produces a view into the original string rather than a copy — so it allocates nothing. strings.index returns -1 when absent.program Substrings;
var
sentence, part: string;
begin
sentence := 'Pascal and Odin';
part := Copy(sentence, 1, 6); { start, count — 1-based }
writeln(part);
writeln(Pos('Odin', sentence)); { 1-based index, 0 if absent }
end. package main
import "core:fmt"
import "core:strings"
main :: proc() {
sentence := "Pascal and Odin"
part := sentence[0:6] // low:high, 0-based, high exclusive
fmt.println(part)
fmt.println(strings.index(sentence, "Odin")) // -1 if absent
fmt.println(strings.contains(sentence, "and"))
} That view semantics is a meaningful difference from
Copy, which builds a new reference-counted string. Note the sentinel too: Pascal's Pos returns 0 for "not found", which Odin cannot use because 0 is a valid index.Building strings incrementally
strings.Builder keeps one growing buffer and fmt.sbprintf formats directly into it. strings.to_string hands back a string viewing that buffer, valid until the builder is destroyed.program BuildString;
uses SysUtils;
var
report: string;
index : Integer;
begin
report := '';
for index := 1 to 5 do
report := report + IntToStr(index) + ' '; { reallocates each time }
writeln(report);
end. package main
import "core:fmt"
import "core:strings"
main :: proc() {
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
for index in 1 ..= 5 {
fmt.sbprintf(&builder, "%d ", index)
}
fmt.println(strings.to_string(builder))
} The Pascal loop opposite reallocates the whole string on every iteration, which is quadratic in the number of appends — a habit worth unlearning. The builder makes it linear.
Iterating characters
Iterating a string with
for … in decodes UTF-8 and yields rune values — full Unicode code points — together with the byte offset each one started at.program IterateString;
var
word : string;
index: Integer;
begin
word := 'Pascal';
for index := 1 to Length(word) do
write(word[index], '.');
writeln;
end. package main
import "core:fmt"
main :: proc() {
word := "Odin"
// Decodes UTF-8 — `character` is a rune, `offset` its byte position
for character, offset in word {
fmt.printf("%v(%d).", character, offset)
}
fmt.println()
} So the offsets advance by more than one for multi-byte characters, unlike the dense 1-to-
Length range a Pascal loop walks. Index the string directly (word[0]) when you genuinely want raw bytes.Arrays, Slices & Maps
Fixed-size arrays
Every Odin array is
[N]T, indexed 0 to N-1 — you cannot choose the index range as Pascal lets you with array[1..5] or array['a'..'z'].program FixedArray;
var
scores: array[1..5] of Integer;
index : Integer;
begin
for index := 1 to 5 do
scores[index] := index * index;
writeln(scores[3], ' length=', Length(scores));
end. package main
import "core:fmt"
main :: proc() {
scores: [5]int
for index in 0 ..< 5 {
scores[index] = (index + 1) * (index + 1)
}
fmt.println(scores, "length =", len(scores))
} In exchange, the length is part of the type, arrays compare with
==, and printing one with fmt.println shows every element rather than requiring a loop.Array literals & iteration
Odin writes array literals inline as
[5]int{…}, so no typed const declaration is needed the way Free Pascal requires. The index is the optional second name in a for … in loop.program ArrayLiteral;
const
Primes: array[0..4] of Integer = (2, 3, 5, 7, 11);
var
value: Integer;
begin
for value in Primes do
write(value, ' ');
writeln;
end. package main
import "core:fmt"
main :: proc() {
primes := [5]int{2, 3, 5, 7, 11}
for value in primes {
fmt.print(value, "")
}
fmt.println()
// The index is the optional SECOND name, not the first
for value, index in primes {
fmt.printf("%d:%d ", index, value)
}
fmt.println()
} Watch that loop-variable order:
for value, index in primes puts the element first and the index second — the reverse of what most languages do, and the single easiest thing to get backwards when starting out.Array-wide arithmetic
Odin's fixed-size arrays are numeric vectors:
+, -, *, and / apply element-wise, and the compiler emits SIMD where the target supports it. The .xyzw swizzles come from shader languages.program ArrayMath;
type
TTriple = array[0..2] of Integer;
const
Left : TTriple = (1, 2, 3);
Right: TTriple = (10, 20, 30);
var
sum : TTriple;
index: Integer;
begin
{ No element-wise operators — you write the loop }
for index := 0 to 2 do
sum[index] := Left[index] + Right[index];
writeln(sum[0], ' ', sum[1], ' ', sum[2]);
end. package main
import "core:fmt"
main :: proc() {
left := [3]int{1, 2, 3}
right := [3]int{10, 20, 30}
// Arithmetic operators apply element-wise to fixed arrays
sum := left + right
scaled := left * 2
fmt.println(sum, scaled)
// Swizzling reorders components, as in shader languages
position := [3]f32{1, 2, 3}
fmt.println(position.zyx, position.xy)
} Pascal has no equivalent — you write the loop, as the anchor column does. The swizzles reflect Odin's origins in graphics and game programming.
Slices
A slice (
[]T) is a pointer plus a length pointing into memory someone else owns. A procedure written against []int works on any array length, on a sub-range, or on a dynamic array — and always knows how many elements it received.program SliceLike;
var
numbers: array[0..5] of Integer = (10, 20, 30, 40, 50, 60);
index : Integer;
begin
{ Pascal has no slice type — pass the array plus a range,
or copy the section you want into a new array. }
for index := 1 to 3 do
write(numbers[index], ' ');
writeln;
end. package main
import "core:fmt"
// A slice parameter accepts any length — the length travels with it
average :: proc(values: []int) -> f64 {
total := 0
for value in values {
total += value
}
return f64(total) / f64(len(values))
}
main :: proc() {
numbers := [6]int{10, 20, 30, 40, 50, 60}
middle := numbers[1:4] // a view: elements 1, 2, 3
fmt.println(middle, len(middle))
fmt.println(average(numbers[:]))
fmt.println(average(middle))
} This is the single biggest ergonomic gain over Pascal here. The Pascal equivalent is either an open array parameter or passing the bounds alongside by hand.
Dynamic arrays
[dynamic]T is a growable array with amortized append and a real cap distinct from len. It is not reference counted, which is why defer delete appears right after the declaration.program DynamicArray;
var
numbers: array of Integer;
index : Integer;
begin
SetLength(numbers, 3);
for index := 0 to High(numbers) do
numbers[index] := index * 10;
SetLength(numbers, 4); { grow, preserving contents }
numbers[3] := 99;
for index := 0 to High(numbers) do
write(numbers[index], ' ');
writeln;
end. package main
import "core:fmt"
main :: proc() {
numbers: [dynamic]int
defer delete(numbers)
append(&numbers, 0, 10, 20)
append(&numbers, 99)
fmt.println(numbers, "len =", len(numbers), "cap =", cap(numbers))
ordered_removal := numbers[:]
fmt.println("as a slice:", ordered_removal)
} Free Pascal's
array of T is reference counted and freed automatically. In return for the manual delete you get append instead of SetLength reallocating on every growth, and the ability to slice with numbers[:] and hand it to any []int procedure.Maps
map[K]V is built into the language. The two-value read value, found := ages[key] is how you distinguish a missing key from one whose value is genuinely zero.program MapLike;
uses fgl;
type
TAgeMap = specialize TFPGMap<string, Integer>;
var
ages: TAgeMap;
begin
ages := TAgeMap.Create;
try
ages['Alice'] := 30;
ages['Bob'] := 25;
writeln('Alice is ', ages['Alice']);
writeln('entries: ', ages.Count);
finally
ages.Free;
end;
end. package main
import "core:fmt"
main :: proc() {
ages := make(map[string]int)
defer delete(ages)
ages["Alice"] = 30
ages["Bob"] = 25
// The comma-ok form distinguishes "absent" from "present but zero"
age, found := ages["Alice"]
fmt.println("Alice is", age, "found:", found)
_, missing := ages["Carol"]
fmt.println("Carol found:", missing)
fmt.println("entries:", len(ages))
} Pascal has no associative array in the language — you reach for
TFPGMap from the fgl unit, with generic-specialization syntax and manual Create/Free. The found flag is worth internalizing: a plain read of a missing key returns the zero value silently.Multidimensional arrays
Odin's
[2][3]int is genuinely an array of arrays, indexed grid[row][column], where Pascal's array[0..1, 0..2] is one rectangular array indexed with a comma.program GridDemo;
var
grid: array[0..1, 0..2] of Integer;
row, column: Integer;
begin
for row := 0 to 1 do
for column := 0 to 2 do
grid[row, column] := row * 3 + column;
for row := 0 to 1 do
begin
for column := 0 to 2 do
write(grid[row, column], ' ');
writeln;
end;
end. package main
import "core:fmt"
main :: proc() {
grid: [2][3]int
for row in 0 ..< 2 {
for column in 0 ..< 3 {
grid[row][column] = row * 3 + column
}
}
for row in grid {
fmt.println(row)
}
} The practical benefit is that a row is itself a value of type
[3]int — it can be printed, compared, assigned, or passed to a procedure on its own, which the Pascal form does not allow.Sets & Enumerations
Enumerated types
Odin enum members are namespaced under the type —
Fruit.Banana, or just .Banana where the type is already known. len and max work on the type itself.program Enumerations;
type
Fruit = (Apple, Banana, Cherry);
var
choice: Fruit;
begin
choice := Banana;
writeln(Ord(choice)); { 1 }
writeln(Ord(High(Fruit))); { 2 }
if choice = Banana then
writeln('it is a banana');
end. package main
import "core:fmt"
Fruit :: enum {
Apple,
Banana,
Cherry,
}
main :: proc() {
choice := Fruit.Banana
fmt.println(int(choice)) // 1
fmt.println(len(Fruit)) // 3 members
fmt.println(max(Fruit)) // Cherry
// %v prints the member NAME, not its ordinal
fmt.printfln("choice is %v", choice)
} Namespacing means two enums may both have an
Apple without colliding, a real limitation of Pascal's flat enumeration scope. Best of all, %v prints the member's name, where Pascal's writeln can only give you the ordinal via Ord.Set types
bit_set[Fruit] is Pascal's set of Fruit — the same packed bit representation, the same in operator. The syntax differs only in that Odin uses braces for the literal and the leading-dot form for members.program Sets;
type
Fruit = (Apple, Banana, Cherry);
FruitSet = set of Fruit;
var
basket: FruitSet;
begin
basket := [Apple, Cherry];
if Apple in basket then
writeln('has an apple');
writeln('banana? ', Banana in basket);
end. package main
import "core:fmt"
Fruit :: enum {
Apple,
Banana,
Cherry,
}
FruitSet :: bit_set[Fruit]
main :: proc() {
basket: FruitSet = {.Apple, .Cherry}
if .Apple in basket {
fmt.println("has an apple")
}
fmt.println("banana?", .Banana in basket)
fmt.println("count:", card(basket))
fmt.println("as a value:", basket)
} This is the clearest piece of Pascal DNA in Odin. Sets are a Wirth idea that almost no modern language kept, and Odin kept it —
card even returns the population count, matching Free Pascal's function of the same name.Set operations
Odin reuses the bitwise operators for set algebra:
| for union, & for intersection, &~ for difference, and <= for the subset test.program SetOperations;
type
Fruit = (Apple, Banana, Cherry);
FruitSet = set of Fruit;
var
mine, yours: FruitSet;
begin
mine := [Apple, Banana];
yours := [Banana, Cherry];
writeln('union has cherry? ', Cherry in (mine + yours));
writeln('intersection banana? ', Banana in (mine * yours));
writeln('difference apple? ', Apple in (mine - yours));
end. package main
import "core:fmt"
Fruit :: enum {Apple, Banana, Cherry}
FruitSet :: bit_set[Fruit]
main :: proc() {
mine: FruitSet = {.Apple, .Banana}
yours: FruitSet = {.Banana, .Cherry}
fmt.println("union: ", mine | yours)
fmt.println("intersection: ", mine & yours)
fmt.println("difference: ", mine &~ yours)
fmt.println("subset? ", mine <= (mine | yours))
} The operations are identical to Pascal's; only the spelling changed, since Pascal overloads the arithmetic operators (
+, *, -) instead. Odin's choice makes the underlying bitfield representation visible.Arrays indexed by an enum
Odin keeps Pascal's enum-indexed array as
[Fruit]int, and iterating it yields (value, member) — the member itself, not an ordinal.program EnumIndexed;
type
Fruit = (Apple, Banana, Cherry);
var
counts: array[Fruit] of Integer;
choice: Fruit;
begin
counts[Apple] := 3;
counts[Cherry] := 5;
for choice := Low(Fruit) to High(Fruit) do
writeln(Ord(choice), ' = ', counts[choice]);
end. package main
import "core:fmt"
Fruit :: enum {Apple, Banana, Cherry}
main :: proc() {
counts: [Fruit]int
counts[.Apple] = 3
counts[.Cherry] = 5
// Iteration yields (value, enum member) — the member is second
for quantity, fruit in counts {
fmt.printfln("%v = %d", fruit, quantity)
}
} So
%v can print Apple instead of 0. The array is guaranteed to have exactly one slot per member, which means adding a new Fruit cannot leave a gap in the table.Control Flow
Conditionals
Odin drops both
then and the parentheses around the condition, and requires braces even for a single statement. The if name := expression; condition form scopes a temporary to the branch.program Conditionals;
var
score: Integer;
begin
score := 75;
if score >= 90 then
writeln('excellent')
else if score >= 70 then
writeln('good')
else
writeln('needs work');
end. package main
import "core:fmt"
main :: proc() {
score := 75
if score >= 90 {
fmt.println("excellent")
} else if score >= 70 {
fmt.println("good")
} else {
fmt.println("needs work")
}
// An initializer may be scoped to the if itself
if doubled := score * 2; doubled > 100 {
fmt.println("doubled past 100:", doubled)
}
} Requiring braces quietly removes Pascal's dangling-
else ambiguity and the "forgot the begin…end" bug. The initializer form has no Pascal equivalent at all.Case & switch
Cases do not fall through, multiple values share a branch with a comma, and
'D' ..= 'F' is a closed range like Pascal's 'D'..'F'. The default branch is a bare case: rather than else.program CaseStatement;
var
grade: Char;
begin
grade := 'B';
case grade of
'A': writeln('excellent');
'B', 'C': writeln('passing');
'D'..'F': writeln('failing');
else
writeln('unknown');
end;
end. package main
import "core:fmt"
main :: proc() {
grade := 'B'
switch grade {
case 'A':
fmt.println("excellent")
case 'B', 'C':
fmt.println("passing") // no fallthrough
case 'D' ..= 'F':
fmt.println("failing")
case:
fmt.println("unknown")
}
} The semantics match Pascal closely — you must write
fallthrough explicitly to get C's behavior, which Pascal never had either.Exhaustive switch over an enum
A
switch over an enum must cover every member or the program does not compile. #partial switch opts out deliberately when incompleteness is intended.program NonExhaustive;
type
Direction = (North, South, East, West);
var
heading: Direction;
begin
heading := East;
{ Omitting a member is silently legal — nothing is printed if
the value falls through with no matching branch. }
case heading of
North: writeln('up');
South: writeln('down');
East: writeln('right');
end;
end. package main
import "core:fmt"
Direction :: enum {North, South, East, West}
main :: proc() {
heading := Direction.East
// Omitting West here is a COMPILE ERROR without #partial
switch heading {
case .North: fmt.println("up")
case .South: fmt.println("down")
case .East: fmt.println("right")
case .West: fmt.println("left")
}
// #partial opts out deliberately, and documents the intent
#partial switch heading {
case .East: fmt.println("still heading east")
}
} This is the safety property Pascal's
case lacks: adding a fifth Direction to a Pascal program leaves every existing case silently incomplete, while in Odin the compiler points at each one. #partial says so in the source rather than leaving the reader to guess.For loops
Odin has one
for keyword covering four shapes. Note the distinction Pascal does not need to make: ..< excludes the upper bound and ..= includes it. There is no downto.program ForLoops;
var
index: Integer;
begin
for index := 1 to 3 do
write(index, ' ');
writeln;
for index := 3 downto 1 do
write(index, ' ');
writeln;
end. package main
import "core:fmt"
main :: proc() {
// Half-open range: 0, 1, 2
for index in 0 ..< 3 {
fmt.print(index, "")
}
fmt.println()
// Closed range: 1, 2, 3 — Pascal's `to`
for index in 1 ..= 3 {
fmt.print(index, "")
}
fmt.println()
// Counting down needs the C-style three-clause form
for index := 3; index >= 1; index -= 1 {
fmt.print(index, "")
}
fmt.println()
} Counting down uses the C-style three-clause form. The four shapes are a range, a three-clause loop, a bare condition, and iteration over a collection.
While & repeat
A
for with a bare condition is Odin's while; a for with nothing at all is an infinite loop. There is no repeat…until — the equivalent is an infinite loop with a break at the bottom.program Loops;
var
countdown: Integer;
begin
countdown := 3;
while countdown > 0 do
begin
write(countdown, ' ');
Dec(countdown);
end;
writeln;
repeat
write('once ');
Inc(countdown);
until countdown >= 1;
writeln;
end. package main
import "core:fmt"
main :: proc() {
countdown := 3
// `for` with a single condition is Odin's while
for countdown > 0 {
fmt.print(countdown, "")
countdown -= 1
}
fmt.println()
// No repeat/until — write the exit test at the bottom
for {
fmt.print("once ")
countdown += 1
if countdown >= 1 {
break
}
}
fmt.println()
} Odin also has no
Inc/Dec procedures and no ++ operator — countdown += 1 is the idiom.Breaking out of nested loops
Odin labels the loop itself, so
break search (or continue search) names which one to act on.program NestedExit;
label
Finished;
var
row, column: Integer;
begin
for row := 0 to 2 do
for column := 0 to 2 do
if row * column > 2 then
begin
writeln('stopped at ', row, ',', column);
goto Finished;
end;
Finished:
writeln('done');
end. package main
import "core:fmt"
main :: proc() {
search: for row in 0 ..< 3 {
for column in 0 ..< 3 {
if row * column > 2 {
fmt.printfln("stopped at %d,%d", row, column)
break search
}
}
}
fmt.println("done")
} Escaping a nested loop in Pascal means declaring a
label and using goto. Here the jump target is always a loop boundary rather than an arbitrary point in the procedure, which keeps the control flow readable.Conditional compilation
when is part of the language, not a preprocessor: the condition is a real typed expression over built-in constants such as ODIN_OS and ODIN_ARCH. Only the taken branch is compiled.program ConditionalCompile;
begin
{$IFDEF DARWIN}
writeln('compiled for macOS');
{$ELSE}
writeln('compiled for something else');
{$ENDIF}
end. package main
import "core:fmt"
main :: proc() {
// `when` is ordinary Odin evaluated at compile time —
// not a preprocessor, so the condition is type-checked.
when ODIN_OS == .Darwin {
fmt.println("compiled for macOS")
} else when ODIN_OS == .Linux {
fmt.println("compiled for Linux")
} else {
fmt.println("compiled for something else")
}
fmt.println("architecture:", ODIN_ARCH)
} Free Pascal's
{$IFDEF} is a directive operating on text before the compiler sees it, so a typo is a branch that silently never fires. In Odin it is a compile error.Procedures & Functions
Procedures & functions
Odin has one keyword,
proc, for both procedures and functions — a procedure is simply one with no -> return type. The value comes back through an explicit return.program Routines;
procedure Greet(name: string);
begin
writeln('Hello, ', name);
end;
function Square(value: Integer): Integer;
begin
Square := value * value; { or: Result := value * value }
end;
begin
Greet('Ada');
writeln(Square(7));
end. package main
import "core:fmt"
greet :: proc(name: string) {
fmt.println("Hello,", name)
}
square :: proc(value: int) -> int {
return value * value
}
main :: proc() {
greet("Ada")
fmt.println(square(7))
} There is no assigning to the function's own name or to
Result, so there is no way to fall off the end of a function having forgotten to set it.Passing by reference
Odin has no
var parameter mode. A procedure that mutates its argument takes a pointer, and the caller writes &count.program ByReference;
procedure Double(var value: Integer);
begin
value := value * 2;
end;
procedure Show(const label_: string; value: Integer);
begin
writeln(label_, ': ', value);
end;
var
count: Integer;
begin
count := 21;
Double(count);
Show('count', count);
end. package main
import "core:fmt"
// No `var` parameters — take a pointer explicitly
double :: proc(value: ^int) {
value^ *= 2
}
show :: proc(label: string, value: int) {
fmt.printfln("%s: %d", label, value)
}
main :: proc() {
count := 21
double(&count)
show("count", count)
} So the call site itself tells you the variable may change, which Pascal's
Double(count) does not. Parameters are otherwise immutable inside the procedure, which is closer to Pascal's const mode than to its plain by-value mode.Multiple return values
Results can be named, are zero-initialized on entry, and are returned by a bare
return — which reads much like assigning to Pascal's Result. Use _ to discard one.program MultipleResults;
procedure DivMod(numerator, denominator: Integer;
var quotient, remainder: Integer);
begin
quotient := numerator div denominator;
remainder := numerator mod denominator;
end;
var
quotient, remainder: Integer;
begin
DivMod(17, 5, quotient, remainder);
writeln(quotient, ' remainder ', remainder);
end. package main
import "core:fmt"
// Results can be named, which documents them at the call site
divide :: proc(numerator, denominator: int) -> (quotient, remainder: int) {
quotient = numerator / denominator
remainder = numerator % denominator
return
}
main :: proc() {
quotient, remainder := divide(17, 5)
fmt.println(quotient, "remainder", remainder)
// Discard the ones you do not need
just_quotient, _ := divide(17, 5)
fmt.println("quotient only:", just_quotient)
} Returning several values is the single change that most reduces Pascal's reliance on
var out-parameters. Odin will not let you silently ignore a result by accident.Default & named arguments
Both languages support default parameter values. Odin adds calling by name:
greet("Carol", punctuation = "?") sets the third parameter while leaving the second at its default.program DefaultArguments;
procedure Greet(name: string; greeting: string = 'Hello');
begin
writeln(greeting, ', ', name);
end;
begin
Greet('Ada');
Greet('Bob', 'Good morning');
{ Free Pascal has no named-argument syntax at the call site }
end. package main
import "core:fmt"
greet :: proc(name: string, greeting := "Hello", punctuation := "!") {
fmt.printfln("%s, %s%s", greeting, name, punctuation)
}
main :: proc() {
greet("Ada")
greet("Bob", "Good morning")
// Named arguments — skip the middle one and set only the last
greet("Carol", punctuation = "?")
} Positional-only Pascal cannot express that. Named arguments may appear in any order, and they make a call with several boolean flags readable without a comment.
Variadic parameters
Odin's
..int collects trailing arguments into a slice, so the call needs no brackets, and ..numbers spreads an existing slice. Inside the procedure values is an ordinary []int.program Variadic;
function SumAll(const values: array of Integer): Integer;
var
index, total: Integer;
begin
total := 0;
for index := 0 to High(values) do
total := total + values[index];
SumAll := total;
end;
begin
writeln(SumAll([1, 2, 3, 4]));
end. package main
import "core:fmt"
sum_all :: proc(values: ..int) -> int {
total := 0
for value in values {
total += value
}
return total
}
main :: proc() {
fmt.println(sum_all(1, 2, 3, 4))
// Spread an existing slice into a variadic call
numbers := []int{5, 6, 7}
fmt.println(sum_all(..numbers))
} Pascal's open array parameter is close in spirit, but the caller must write the values as an array constructor.
Procedures as values
A procedure type is written exactly like a signature with the body omitted, and passing one needs no
@ address-of operator. Anonymous procedure literals exist too.program ProcedureValues;
type
Transform = function(value: Integer): Integer;
function Double(value: Integer): Integer;
begin
Double := value * 2;
end;
function ApplyTo(operation: Transform; value: Integer): Integer;
begin
ApplyTo := operation(value);
end;
begin
writeln(ApplyTo(@Double, 21));
end. package main
import "core:fmt"
Transform :: proc(value: int) -> int
double :: proc(value: int) -> int {
return value * 2
}
apply_to :: proc(operation: Transform, value: int) -> int {
return operation(value)
}
main :: proc() {
fmt.println(apply_to(double, 21))
// An anonymous procedure literal, assigned like any other value
triple := proc(value: int) -> int { return value * 3 }
fmt.println(apply_to(triple, 21))
} They are not closures — a literal cannot capture surrounding local variables, which keeps a procedure value a bare code pointer with no hidden allocation behind it.
Records & Structs
Records & structs
A struct literal names its fields, every struct is zero-initialized, and
%v prints the whole thing with field names. Structs also compare with == when every field does.program Records;
type
TPoint = record
X, Y: Double;
end;
var
origin: TPoint;
begin
origin.X := 3.0;
origin.Y := 4.0;
writeln('(', origin.X:3:1, ', ', origin.Y:3:1, ')');
end. package main
import "core:fmt"
Point :: struct {
x: f64,
y: f64,
}
main :: proc() {
// Field names make the literal self-documenting
origin := Point{x = 3, y = 4}
fmt.printfln("(%.1f, %.1f)", origin.x, origin.y)
// Every struct is zero-initialized
blank: Point
fmt.println("blank:", blank)
// %v prints the whole struct, field names and all
fmt.printfln("%v", origin)
} Those are the ergonomic gains over Pascal records: the reader does not have to remember declaration order, and there is no per-field
writeln. Both languages give you value semantics — assigning a struct copies it.with vs using (deprecated)
Odin had exactly Pascal's
with, spelled using, and then took it away. A bare using person statement and using on a procedure parameter are both rejected by the current compiler; the #+feature using-stmt directive above is required to enable it at all.program WithStatement;
type
TPerson = record
Name: string;
Age : Integer;
end;
var
person: TPerson;
begin
with person do
begin
Name := 'Ada';
Age := 36;
writeln(Name, ' is ', Age);
end;
end. #+feature using-stmt
package main
import "core:fmt"
Person :: struct {
name: string,
age: int,
}
describe :: proc(person: Person) {
// Opens the fields into scope, exactly as Pascal's with does —
// but the #+feature line above is required to enable it at all.
using person
fmt.println(name, "is", age)
}
main :: proc() {
person := Person{name = "Ada", age = 36}
describe(person)
} The compiler's own words are "disallowed as it is considered bad practice … outside of immediate refactoring" — for exactly the hazard every Pascal programmer knows, where after
with an unqualified name may silently mean a field instead of the local you were looking at. What survives, and is still encouraged, is using on a struct field — the next row.Composing records
Applying
using to a struct field promotes that field's members into the outer struct, so worker.name resolves through to worker.identity.name while the full path keeps working.program Composition;
type
TNamed = record
Name: string;
end;
TEmployee = record
Identity: TNamed; { must be reached through the field name }
Salary : Integer;
end;
var
worker: TEmployee;
begin
worker.Identity.Name := 'Ada';
worker.Salary := 100;
writeln(worker.Identity.Name, ' earns ', worker.Salary);
end. package main
import "core:fmt"
Named :: struct {
name: string,
}
Employee :: struct {
using identity: Named, // fields promoted into Employee
salary: int,
}
main :: proc() {
worker := Employee{identity = Named{name = "Ada"}, salary = 100}
// Reachable directly, without naming the embedded field
fmt.println(worker.name, "earns", worker.salary)
// The full path still works, and the sub-record is still a value
fmt.println(worker.identity)
} This gives Odin composition without inheritance — the embedded value is still a plain field you can pass to any procedure expecting a
Named. Pascal has no equivalent for records; you would reach for a class and inherit.Methods on a type
Odin has no methods and no receiver syntax —
box.Area has no counterpart. A procedure that operates on a Rectangle simply takes one, conventionally named after the type.{$modeswitch advancedrecords}
program MethodsDemo;
type
TRectangle = record
Width, Height: Double;
function Area: Double;
end;
function TRectangle.Area: Double;
begin
Area := Width * Height;
end;
var
box: TRectangle;
begin
box.Width := 3;
box.Height := 4;
writeln(box.Area:5:1);
end. package main
import "core:fmt"
Rectangle :: struct {
width: f64,
height: f64,
}
// No methods — an ordinary procedure taking the value
rectangle_area :: proc(rectangle: Rectangle) -> f64 {
return rectangle.width * rectangle.height
}
// Take a pointer when the procedure must mutate
rectangle_scale :: proc(rectangle: ^Rectangle, factor: f64) {
rectangle.width *= factor
rectangle.height *= factor
}
main :: proc() {
box := Rectangle{width = 3, height = 4}
fmt.printfln("%.1f", rectangle_area(box))
rectangle_scale(&box, 2)
fmt.printfln("%.1f", rectangle_area(box))
} Note that
rectangle.width works on a ^Rectangle without an explicit ^: Odin auto-dereferences pointers on field access, as Free Pascal does for class instances. The Pascal column needs {$modeswitch advancedrecords} to put a method on a record at all.Memory layout control
Pascal's
packed record is Odin's struct #packed. Odin adds #align(N) to force a minimum alignment, plus align_of and offset_of to inspect the result.program Layout;
type
TNormal = record
Flag : Byte;
Value: LongInt;
end;
TPacked = packed record
Flag : Byte;
Value: LongInt;
end;
begin
writeln('normal: ', SizeOf(TNormal));
writeln('packed: ', SizeOf(TPacked));
end. package main
import "core:fmt"
Normal :: struct {
flag: u8,
value: i32,
}
Packed :: struct #packed {
flag: u8,
value: i32,
}
Aligned :: struct #align(16) {
flag: u8,
value: i32,
}
main :: proc() {
fmt.println("normal: ", size_of(Normal))
fmt.println("packed: ", size_of(Packed))
fmt.println("aligned:", size_of(Aligned), "align", align_of(Aligned))
fmt.println("offset of value:", offset_of(Normal, value))
} Both let you strip padding for a struct that must match an on-disk or on-the-wire layout. The introspection matters when you are matching a C header rather than guessing.
Pointers & Memory
Pointers
Of all the syntax Odin inherited, this is the most literal: the pointer type is
^T and dereference is postfix ^, both taken straight from Pascal. The only spelling difference is &value where Pascal writes @value.program Pointers;
var
value : Integer;
pointer: ^Integer;
begin
value := 10;
pointer := @value;
pointer^ := 42; { dereference with ^ }
writeln(value);
writeln(pointer = nil);
end. package main
import "core:fmt"
main :: proc() {
value := 10
pointer: ^int = &value
pointer^ = 42 // dereference with ^, exactly as in Pascal
fmt.println(value)
fmt.println(pointer == nil)
} Both languages spell the null pointer
nil. Odin also has no pointer arithmetic in ordinary code: to walk memory you use a slice, which carries its length and is bounds-checked in debug builds.Heap allocation
new(Node) is Pascal's New and free is Dispose. The memory is zeroed rather than holding garbage, and field access auto-dereferences, so head.value means head^.value.program HeapAllocation;
type
PNode = ^TNode;
TNode = record
Value: Integer;
Next : PNode;
end;
var
head: PNode;
begin
New(head);
head^.Value := 42;
head^.Next := nil;
writeln(head^.Value);
Dispose(head);
end. package main
import "core:fmt"
Node :: struct {
value: int,
next: ^Node,
}
main :: proc() {
head := new(Node)
defer free(head)
head.value = 42 // auto-dereference: no ^ needed for fields
head.next = nil
fmt.println(head.value)
} A forward type declaration is unnecessary too —
Node may refer to ^Node before the declaration completes, so Pascal's PNode = ^TNode dance is not needed.defer
defer runs a statement at scope exit by any path, in reverse order — the counterpart to try…finally. The important part is placement: the cleanup sits on the line after the allocation.program Cleanup;
var
buffer: ^Integer;
begin
New(buffer);
try
buffer^ := 7;
writeln('using ', buffer^);
finally
Dispose(buffer); { runs however the block exits }
end;
end. package main
import "core:fmt"
main :: proc() {
buffer := new(int)
defer free(buffer) // written beside the allocation
buffer^ = 7
fmt.println("using", buffer^)
// Deferred statements run in reverse order at scope exit
defer fmt.println("second deferred, printed first")
defer fmt.println("first deferred, printed second")
fmt.println("end of main body")
} So an allocation with no matching release is visible at a glance, rather than sitting several screens below it. Note that a
defer the compiler can prove is unreachable — after os.exit, for instance — is a compile error rather than a silent no-op.The implicit context
Every Odin procedure receives an implicit
context holding an allocator, a temp allocator, and a logger — you never declare or pass it. Assigning context.allocator redirects allocation for the current scope and everything it calls, which is why collect uses the arena without a signature change.program CustomMemory;
{ Free Pascal's memory manager is global: to change allocation
strategy you install a different TMemoryManager process-wide
with SetMemoryManager, or you thread your own allocator
through every routine by hand. There is no scoped mechanism. }
begin
writeln('default heap only');
end. package main
import "core:fmt"
import "core:mem"
collect :: proc() -> []int {
// Allocates from context.allocator, whatever the caller set it to
values := make([]int, 3)
values[0] = 1
return values
}
main :: proc() {
backing: [1024]byte
arena: mem.Arena
mem.arena_init(&arena, backing[:])
// Redirect every allocation below this point — including inside
// collect(), which was never told about the arena.
context.allocator = mem.arena_allocator(&arena)
values := collect()
fmt.println("from the arena:", values)
fmt.println("no individual frees needed")
} This is the feature with no Pascal counterpart at all, and the one most worth understanding. Free Pascal's memory manager is global: you install a different
TMemoryManager process-wide or thread your own allocator through every routine by hand. The arena here is a fixed byte buffer, so nothing needs freeing individually.The temporary allocator
The
context carries a second allocator meant for short-lived scratch values. Anything taken from it is released by a single free_all(context.temp_allocator) rather than one delete per value.program ShortLived;
uses SysUtils;
var
message: string;
begin
{ Reference counting frees this when it goes out of scope —
convenient, but the cost is spread invisibly across the program. }
message := 'value is ' + IntToStr(42);
writeln(message);
end. package main
import "core:fmt"
describe :: proc(value: int) -> string {
// Scratch memory: no individual free, reclaimed in one go
return fmt.aprintf("value is %d", value, allocator = context.temp_allocator)
}
main :: proc() {
defer free_all(context.temp_allocator)
for value in 1 ..= 3 {
fmt.println(describe(value))
}
// One free_all reclaims all three strings at once
} That removes most of the bookkeeping that makes manual memory tedious. It is the closest thing Odin has to Free Pascal's automatic string management, except you decide exactly when the reclamation happens.
Variant Records & Unions
Variant records vs tagged unions
An Odin
union stores a hidden tag alongside the payload and checks it on every access. The type-assertion form shape.(Circle) returns the value plus an ok flag.program VariantRecord;
type
ShapeKind = (CircleShape, RectangleShape);
TShape = record
case Kind: ShapeKind of
CircleShape: (Radius: Double);
RectangleShape: (Width, Height: Double);
end;
var
shape: TShape;
begin
shape.Kind := CircleShape;
shape.Radius := 2.0;
{ Nothing stops you reading shape.Width here — it returns garbage }
writeln(shape.Radius:4:1);
end. package main
import "core:fmt"
Circle :: struct {
radius: f64,
}
Rectangle :: struct {
width: f64,
height: f64,
}
Shape :: union {
Circle,
Rectangle,
}
main :: proc() {
shape: Shape = Circle{radius = 2}
// Reading the wrong variant is checked, not undefined
circle, ok := shape.(Circle)
fmt.println("is a circle:", ok, "radius:", circle.radius)
_, is_rectangle := shape.(Rectangle)
fmt.println("is a rectangle:", is_rectangle)
} Pascal's variant record shares storage between alternatives but does not enforce which one is live — the
case tag is documentation the compiler does not check, so reading the wrong variant reinterprets unrelated bytes. A union also has a nil state meaning "no variant set yet", which a variant record cannot represent.Switching on the variant
The
switch value in union form binds a differently typed variable in each branch — inside case Circle the name specific is a Circle, so specific.width would not compile.program VariantDispatch;
type
ShapeKind = (CircleShape, RectangleShape);
TShape = record
case Kind: ShapeKind of
CircleShape: (Radius: Double);
RectangleShape: (Width, Height: Double);
end;
function Area(const shape: TShape): Double;
begin
case shape.Kind of
CircleShape: Area := 3.14159 * shape.Radius * shape.Radius;
RectangleShape: Area := shape.Width * shape.Height;
end;
end;
var
circle: TShape;
begin
circle.Kind := CircleShape;
circle.Radius := 2.0;
writeln(Area(circle):6:2);
end. package main
import "core:fmt"
Circle :: struct { radius: f64 }
Rectangle :: struct { width, height: f64 }
Shape :: union {
Circle,
Rectangle,
}
area :: proc(shape: Shape) -> f64 {
// `specific` is typed per branch — no field-name guessing
switch specific in shape {
case Circle:
return 3.14159 * specific.radius * specific.radius
case Rectangle:
return specific.width * specific.height
}
return 0
}
main :: proc() {
fmt.printfln("%.2f", area(Circle{radius = 2}))
fmt.printfln("%.2f", area(Rectangle{width = 3, height = 4}))
} Pascal's equivalent switches on a tag you maintain by hand and then reads fields the compiler never associated with that tag. A mistake there is silent; here it is impossible.
Optional values
Maybe(T) is a union of T and nothing, so "absent" is a state of the type rather than a sentinel the caller has to know about. The .? suffix unwraps it into a value and an ok flag.program OptionalValue;
{ Pascal has no optional type. The usual approaches are a
sentinel value (-1) or a separate Boolean "found" flag. }
function FindIndex(const values: array of Integer;
wanted: Integer): Integer;
var
index: Integer;
begin
FindIndex := -1;
for index := 0 to High(values) do
if values[index] = wanted then
begin
FindIndex := index;
Exit;
end;
end;
var
numbers: array[0..2] of Integer = (10, 20, 30);
begin
writeln(FindIndex(numbers, 20));
writeln(FindIndex(numbers, 99)); { -1 means "absent" — by convention }
end. package main
import "core:fmt"
find_index :: proc(values: []int, wanted: int) -> Maybe(int) {
for value, index in values {
if value == wanted {
return index
}
}
return nil
}
main :: proc() {
numbers := []int{10, 20, 30}
if index, found := find_index(numbers, 20).?; found {
fmt.println("found at", index)
}
result := find_index(numbers, 99)
fmt.println("absent is nil:", result == nil)
} This matters because a sentinel like
-1 is only safe while -1 is not a legitimate result — with Maybe, the whole range of int stays available.Error Handling
No exceptions
Odin has no exceptions, no
try…except, and no stack unwinding. A procedure that can fail says so in its return type.program Exceptions;
uses SysUtils;
var
numerator, denominator, quotient: Integer;
begin
numerator := 10;
denominator := 0;
try
quotient := numerator div denominator;
writeln(quotient);
except
on E: EDivByZero do
writeln('caught: division by zero');
end;
end. package main
import "core:fmt"
Math_Error :: enum {
None,
Division_By_Zero,
}
divide :: proc(numerator, denominator: int) -> (int, Math_Error) {
if denominator == 0 {
return 0, .Division_By_Zero
}
return numerator / denominator, .None
}
main :: proc() {
quotient, error := divide(10, 0)
if error != .None {
fmt.println("handled:", error)
} else {
fmt.println(quotient)
}
} So the failure is visible in the signature rather than discovered at runtime. The trade-off is real: Pascal can let an error propagate up several frames untouched, while in Odin each frame must pass it along — which the
or_return operator below exists to make painless.Errors as enum values
Odin returns the value and the error together, so no
var out-parameter is needed. Naming the zero member None is the community convention.program ErrorCodes;
uses SysUtils;
type
TParseError = (peNone, peNotANumber, peOutOfRange);
function ParsePositive(const text: string;
var value: Integer): TParseError;
begin
if not TryStrToInt(text, value) then
ParsePositive := peNotANumber
else if value <= 0 then
ParsePositive := peOutOfRange
else
ParsePositive := peNone;
end;
var
parsed: Integer;
status: TParseError;
begin
status := ParsePositive('42', parsed);
writeln(Ord(status), ' ', parsed);
status := ParsePositive('abc', parsed);
writeln(Ord(status));
end. package main
import "core:fmt"
import "core:strconv"
Parse_Error :: enum {
None,
Not_A_Number,
Out_Of_Range,
}
parse_positive :: proc(text: string) -> (value: int, error: Parse_Error) {
parsed, ok := strconv.parse_int(text)
if !ok {
return 0, .Not_A_Number
}
if parsed <= 0 {
return 0, .Out_Of_Range
}
return parsed, .None
}
main :: proc() {
value, error := parse_positive("42")
fmt.println(value, error)
_, failure := parse_positive("abc")
fmt.println("failure:", failure)
} The shape is one an experienced Pascal programmer already writes by hand — an error enum returned as a status code — but with the value returned alongside it, and an unset error meaning "no error".
Propagating errors with or_return
or_return takes the last returned value as the error and, if it is not the zero value, returns immediately from the enclosing procedure passing that error through.program Propagate;
uses SysUtils;
type
TParseError = (peNone, peNotANumber);
function ParseValue(const text: string; var value: Integer): TParseError;
begin
if TryStrToInt(text, value) then ParseValue := peNone
else ParseValue := peNotANumber;
end;
function DoubleValue(const text: string; var value: Integer): TParseError;
var
status: TParseError;
begin
status := ParseValue(text, value); { check every call by hand }
if status <> peNone then
begin
DoubleValue := status;
Exit;
end;
value := value * 2;
DoubleValue := peNone;
end;
var
result_: Integer;
status : TParseError;
begin
status := DoubleValue('21', result_);
writeln(Ord(status), ' ', result_);
end. package main
import "core:fmt"
import "core:strconv"
Parse_Error :: enum {None, Not_A_Number}
parse_value :: proc(text: string) -> (value: int, error: Parse_Error) {
parsed, ok := strconv.parse_int(text)
if !ok {
return 0, .Not_A_Number
}
return parsed, .None
}
double_value :: proc(text: string) -> (result: int, error: Parse_Error) {
// Returns early with the same error if parse_value fails
value := parse_value(text) or_return
return value * 2, .None
}
main :: proc() {
result, error := double_value("21")
fmt.println(result, error)
_, failure := double_value("nope")
fmt.println("failure:", failure)
} This is the piece that makes return-code error handling bearable: the five-line manual check in the Pascal column collapses to a suffix. Because the enclosing procedure's results are named, the compiler knows what to return.
Defaults with or_else
or_else supplies a fallback inline for anything returning a value plus an ok flag or error — including a map lookup, as in ages["Carol"] or_else 0.program Fallback;
uses SysUtils;
var
port: Integer;
begin
if not TryStrToInt('not-a-port', port) then
port := 8080; { fall back to the default }
writeln('port ', port);
end. package main
import "core:fmt"
import "core:strconv"
parse_port :: proc(text: string) -> (int, bool) {
return strconv.parse_int(text)
}
main :: proc() {
// Supply the fallback inline — no temporary, no if
port := parse_port("not-a-port") or_else 8080
fmt.println("port", port)
good := parse_port("9000") or_else 8080
fmt.println("port", good)
} Where
or_return propagates a failure, or_else swallows it and substitutes a default. The Pascal version needs a mutable variable and an if to say the same thing.Assertions & aborting
assert mirrors Pascal's and is removed by the optimizer in release builds. os.exit is Halt, and panic("message") always aborts with a stack trace.program Abort;
var
count: Integer;
begin
count := 5;
Assert(count > 0, 'count must be positive');
writeln('count is ', count);
if count > 3 then
begin
writeln('aborting with status 1');
Halt(1);
end;
end. package main
import "core:fmt"
import "core:os"
main :: proc() {
count := 5
// Compiled out in release builds
assert(count > 0, "count must be positive")
fmt.println("count is", count)
if count > 3 {
fmt.println("aborting with status 1")
os.exit(1)
}
} One difference to watch: because
os.exit never returns, the compiler rejects any defer that could only run after it — an unreachable cleanup is an error here rather than something you discover in a leak report.Units & Packages
Units vs packages
An Odin package is a whole directory, and every
.odin file in it shares a single namespace with no import between them. Visibility is the @(private) attribute rather than which section a declaration sits in.unit Geometry;
interface { the public part }
type
TPoint = record
X, Y: Double;
end;
function Distance(const a, b: TPoint): Double;
implementation { the private part }
uses Math;
function Distance(const a, b: TPoint): Double;
begin
Distance := Sqrt(Sqr(b.X - a.X) + Sqr(b.Y - a.Y));
end;
end. // File: geometry/geometry.odin
// A package is a DIRECTORY. Every .odin file in it shares one
// namespace, so there is no interface/implementation split.
package geometry
import "core:math"
Point :: struct {
x: f64,
y: f64,
}
// Capitalization carries no meaning; everything is exported
// unless marked otherwise.
distance :: proc(a, b: Point) -> f64 {
return math.sqrt(math.pow(b.x - a.x, 2) + math.pow(b.y - a.y, 2))
}
@(private)
helper :: proc() {} // visible only inside this package A Pascal unit is one file split into an
interface section and an implementation section. In Odin the declaration you want is never behind a header, and capitalization carries no meaning.Using another unit
Odin imports are always qualified: after
import "core:math" you write math.sin, never a bare Sin. The core: prefix names the standard library, and an alias renames a package locally.program UseUnits;
uses
SysUtils, Math;
var
angle: Double;
begin
angle := Pi / 4;
writeln(Format('%.4f', [Sin(angle)]));
writeln(IntToStr(Max(3, 7)));
end. package main
import "core:fmt"
import "core:math"
import linear "core:math/linalg" // aliased
main :: proc() {
angle := math.PI / 4
fmt.printfln("%.4f", math.sin(angle))
fmt.println(max(3, 7))
direction := linear.Vector2f32{1, 0}
fmt.println("length:", linear.length(direction))
} Pascal's
uses dumps every name into the current scope, so two units exporting the same identifier collide and the later one silently wins.Parametric Polymorphism
Generic procedures
The
$ prefix marks a parameter the compiler should infer and specialize on, so largest is called like any other procedure — no specialize keyword and no angle brackets at the call site.program GenericProcedure;
generic function Largest<T>(const values: array of T): T;
var
index: Integer;
begin
{ Inside a generic, the routine name denotes the generic itself,
so the result must be assigned through Result. }
Result := values[0];
for index := 1 to High(values) do
if values[index] > Result then
Result := values[index];
end;
begin
writeln(specialize Largest<Integer>([3, 17, 8]));
end. package main
import "core:fmt"
// $T is inferred from the argument — no explicit specialization
largest :: proc(values: []$T) -> T {
best := values[0]
for value in values[1:] {
if value > best {
best = value
}
}
return best
}
main :: proc() {
fmt.println(largest([]int{3, 17, 8}))
fmt.println(largest([]f64{1.5, 0.5, 2.25}))
fmt.println(largest([]string{"pear", "apple"}))
} Each distinct type instantiates a separate copy at compile time, exactly as Free Pascal's generics do, but the type argument is deduced rather than written out. Note the Pascal column must assign to
Result: inside a generic, the routine name denotes the generic itself.Generic data structures
A generic struct takes its parameter as
$T: typeid and is instantiated by writing Stack(int). Procedures over it declare the parameter in the same position — ^Stack($T) — which both constrains the argument and binds T for the body.program GenericStack;
uses fgl;
type
TIntegerStack = specialize TFPGList<Integer>;
var
stack: TIntegerStack;
begin
stack := TIntegerStack.Create;
try
stack.Add(10);
stack.Add(20);
writeln(stack[stack.Count - 1], ' count=', stack.Count);
finally
stack.Free;
end;
end. package main
import "core:fmt"
Stack :: struct($T: typeid) {
items: [dynamic]T,
}
// Named stack_push / stack_pop because pop is already a builtin
stack_push :: proc(stack: ^Stack($T), value: T) {
append(&stack.items, value)
}
stack_pop :: proc(stack: ^Stack($T)) -> (value: T, ok: bool) {
if len(stack.items) == 0 {
return
}
return pop(&stack.items), true
}
main :: proc() {
numbers: Stack(int)
defer delete(numbers.items)
stack_push(&numbers, 10)
stack_push(&numbers, 20)
top, ok := stack_pop(&numbers)
fmt.println(top, ok, "remaining:", len(numbers.items))
} Compare the Pascal column's
specialize TFPGList<Integer> plus Create/Free: Odin's version is a plain value with no class machinery. The procedures are named stack_push/stack_pop because pop is already a builtin.Compile-time value parameters
[$N]int matches an array of any length and makes that length available as the compile-time constant N inside the body — including in the return type.program FixedLength;
{ Pascal generics parameterise over TYPES only. An array length
cannot be a generic parameter, so a routine taking a fixed-size
array must name one exact length — or accept an open array and
lose the compile-time size. }
type
TTriple = array[0..2] of Integer;
function SumThree(const values: TTriple): Integer;
begin
SumThree := values[0] + values[1] + values[2];
end;
var
triple: TTriple = (1, 2, 3);
begin
writeln(SumThree(triple));
end. package main
import "core:fmt"
// $N binds the array LENGTH — a value, not a type
sum_fixed :: proc(values: [$N]int) -> int {
total := 0
for value in values {
total += value
}
return total
}
// The length is usable in the body and in the return type
doubled :: proc(values: [$N]int) -> [N]int {
result: [N]int
for value, index in values {
result[index] = value * 2
}
return result
}
main :: proc() {
fmt.println(sum_fixed([3]int{1, 2, 3}))
fmt.println(sum_fixed([5]int{1, 2, 3, 4, 5}))
fmt.println(doubled([3]int{1, 2, 3}))
} Odin's parametric polymorphism binds values as well as types, so
doubled gives back an array of exactly the length it received. Pascal generics parameterize over types only, so the closest equivalent loses the compile-time length.Data-Oriented Design
Struct of arrays
Prefixing an array type with
#soa tells Odin to store the fields as separate columns while keeping entities[index].field syntax. The memory layout changes; the code reading it does not.program StructOfArrays;
type
TEntity = record
X, Y : Single;
Health: Integer;
end;
{ Array of structs — the layout you get by default }
TEntities = array[0..3] of TEntity;
{ A struct of arrays must be declared as a SEPARATE type,
and every access site has to be rewritten to match. }
TEntitySoA = record
X, Y : array[0..3] of Single;
Health: array[0..3] of Integer;
end;
var
packed_: TEntitySoA;
begin
packed_.Health[0] := 50;
writeln(packed_.Health[0]);
end. package main
import "core:fmt"
Entity :: struct {
x, y: f32,
health: int,
}
main :: proc() {
// Array of structs — one Entity after another in memory
array_of_structs: [4]Entity
array_of_structs[0].health = 50
// Struct of arrays — every x together, every health together.
// The indexing syntax is IDENTICAL.
struct_of_arrays: #soa[4]Entity
struct_of_arrays[0].health = 50
struct_of_arrays[1].health = 75
fmt.println(array_of_structs[0].health)
fmt.println(struct_of_arrays[0].health, struct_of_arrays[1].health)
// The underlying columns are reachable as slices
fmt.println("health column:", struct_of_arrays.health)
} Reorganizing an array of records into parallel arrays — so that a loop touching one field reads contiguous memory instead of striding past the others — is a standard optimization. In Pascal it means a new type and rewriting every access site. This is Odin's signature feature for game and simulation work, and it has no equivalent in Pascal, C, or Go.
Allocation-free procedures
Because the allocator is a value in the
context, a procedure can be run against a fixed stack buffer and will fail loudly rather than silently reaching for the heap. fmt.bprintf formats into a caller-supplied buffer and allocates nothing at all.program NoAllocation;
uses SysUtils;
var
report: string;
index : Integer;
begin
{ Every concatenation touches the heap, invisibly }
report := '';
for index := 1 to 3 do
report := report + IntToStr(index) + ' ';
writeln(report);
end. package main
import "core:fmt"
import "core:mem"
main :: proc() {
// Refuse to touch the heap: any allocation now fails loudly
backing: [512]byte
arena: mem.Arena
mem.arena_init(&arena, backing[:])
context.allocator = mem.arena_allocator(&arena)
builder_backing: [128]byte
written := 0
for index in 1 ..= 3 {
count := len(fmt.bprintf(builder_backing[written:], "%d ", index))
written += count
}
fmt.println(string(builder_backing[:written]))
fmt.println("bytes used from the stack buffer:", written)
} That is useful in an audio callback, an interrupt handler, or a frame budget, where the guarantee you want is not "allocates rarely" but "cannot allocate". The Pascal loop opposite allocates on every iteration and gives you no way to say "not here".
Finding leaks
Because an allocator is an ordinary value, wrapping one is just composition.
Tracking_Allocator records every allocation and which source location made it, then reports whatever was never freed.program LeakDetection;
{ Free Pascal ships heaptrc: compile with -gh and the runtime
prints unfreed blocks at exit.
fpc -gh leak.pas && ./leak
It is a compiler switch, so it applies to the whole program
and cannot be scoped to one section of code. }
var
leaked: ^Integer;
begin
New(leaked);
leaked^ := 42;
writeln(leaked^);
{ Dispose deliberately omitted }
end. package main
import "core:fmt"
import "core:mem"
main :: proc() {
tracker: mem.Tracking_Allocator
mem.tracking_allocator_init(&tracker, context.allocator)
defer mem.tracking_allocator_destroy(&tracker)
context.allocator = mem.tracking_allocator(&tracker)
leaked := new(int)
leaked^ = 42
fmt.println(leaked^)
// free(leaked) deliberately omitted
for _, entry in tracker.allocation_map {
fmt.printfln("leaked %d bytes at %v", entry.size, entry.location)
}
} Unlike Free Pascal's
-gh switch this is scoped — you can track one subsystem and leave the rest alone — and it needs no special build, so it works the same in a release binary.