PONY λ M2 Modula-2

Pascal.CodeCompared.To/Odin

An interactive executable cheatsheet comparing Pascal and Odin

Free Pascal 3.2.2 Odin 2026-07a
Program Structure & Output
Hello, World
Odin's entry point is a procedure named main inside package main, rather than Pascal's bare beginend. 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 beginend" 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 repeatuntil — 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 tryfinally. 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 tryexcept, 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.