Odin — Reference
Source: https://odin-lang.org/docs/overview/
Odin
- Created: 2016 by Ginger Bill (Bill Hall); first public release July 2016
- Latest stable: dev-2026-05 (2026-05-04). Odin uses monthly rolling “dev-YYYY-MM” releases — no semver
- Status: Beta-ish, no 1.0 yet. Core language stable; standard library evolves (notable transition to “new
core:os” finalizing late 2025/early 2026). Used in shipping commercial games - Paradigms: Procedural, imperative, data-oriented; explicit over implicit; “C with better ergonomics, no OOP”
- Typing: Static, strong, structural-ish, parametric polymorphism (compile-time generics), distinct types, tagged unions, bit_sets, bit_fields, SOA
- Memory: No GC. Manual via explicit allocators; first-class
context.allocatorsystem; bounds-checked by default - Compilation: AOT, LLVM-backed (own front end). Targets Linux/macOS/Windows/FreeBSD/OpenBSD on amd64/arm64; WebAssembly (WASI/freestanding); Orca platform
- Primary domains: Game development, graphics/rendering, systems, performance-critical apps, FFI-heavy projects
- Official docs: https://odin-lang.org/docs/overview/ · https://pkg.odin-lang.org/
At a glance
Odin is Bill Hall’s love letter to Pascal/C — a low-level, joyful systems language that gives you C’s control with modern type-system ergonomics, plus the design choices that make games fun to write (SOA, bit_set, matrix types, swizzling, custom allocators in implicit context). The largest production user is EmberGen / JangaFX, which uses Odin commercially. The language has no garbage collector, no exceptions, no runtime type erasure, no hidden allocation, and no operator overloading. Build configuration is “Odin programs that emit Odin programs” — there’s no separate build system to learn.
Getting started
Install:
# Linux/macOS
git clone https://github.com/odin-lang/Odin
cd Odin && make release-native
# Add ./odin to PATH
odin version # → dev-2026-05:hash
# Windows: download from https://odin-lang.org/ or use winget
winget install --id GingerBill.OdinNo version manager — git pull && make to update. Pin a tag for reproducible builds.
Hello world (hello.odin):
package main
import "core:fmt"
main :: proc() {
fmt.println("Hellope!")
}Run: odin run . (compiles all .odin files in dir as the main package).
Project layout:
myapp/
├── main.odin # package main
├── util/
│ └── util.odin # package util
└── vendor_libs/ # if you vendor anything
Package = directory. All .odin files in a directory must declare the same package name. Imports use slash paths: import "util", import "core:fmt", import "vendor:raylib".
Build / package tool: odin is the toolchain. Common commands:
odin build .— build a binaryodin run .— build + runodin test .— run@(test)-marked procsodin doc .— generate docsodin check .— type-check without codegenodin strip-semicolon .— fix styleodin -file foo.odin— treat single file as ad-hoc package
There is no central package registry by design. Dependencies are vendored as Git submodules or copied into a shared/ directory. core: is the standard library; vendor: ships pre-bound third-party libs (raylib, vulkan, OpenGL, sdl2/sdl3, miniaudio, stb, glfw, wgpu, directx, ENet).
REPL: None — Odin has no interactive REPL. Use odin -file scratch.odin for scripts.
Basics
Types & literals:
- Booleans:
bool,b8 b16 b32 b64 - Integers:
int uint(word-sized) plusi8…i128,u8…u128, plus endian-expliciti32le,u64be, etc. - Floats:
f16 f32 f64; complexcomplex32/64/128; quaternionsquaternion64/128/256 rune(alias fori32, Unicode code point),string(immutable, UTF-8, ptr+len),cstring(NUL-terminated for FFI)- Aggregate: arrays
[N]T, slices[]T, dynamic arrays[dynamic]T, mapsmap[K]V, structs, taggedunions,enums,bit_set[E],bit_field - Literals:
1_000_000,0xFF,0b1010,0o17,'a'(rune),"str",\raw“
Variables & scoping: Use := for declaration with inference, : with type, :: for compile-time constants:
x := 42 // mutable, inferred int
y: f32 = 3.14 // typed
PI :: 3.14159 // compile-time constant
Foo :: struct { x: int } // type also via ::
add :: proc(a, b: int) -> int { return a + b }:: declares anything known at compile time (constants, types, procs, packages). _ is the discard identifier. --- means “uninitialized” (skips zero-init); plain declarations always zero-initialize.
Control flow:
if cond { ... } else if other { ... } else { ... }
for i := 0; i < 10; i += 1 { ... }
for i in 0..<10 { ... } // half-open
for i in 0..=10 { ... } // closed
for v, i in slice { ... } // index, value enumeration
for cond { ... } // while
for { ... } // infiniteswitch is a switch-on-anything (incl. types via switch type x in foo):
switch x {
case 1, 2: fmt.println("small")
case 3..=5: fmt.println("mid")
case: fmt.println("else")
}when is compile-time if (replaces #ifdef).
Functions (procedures):
add :: proc(a, b: int) -> int { return a + b }
divmod :: proc(a, b: int) -> (q, r: int) { return a/b, a%b } // multiple named returns
greet :: proc(name: string = "world") { fmt.printf("hi %s\n", name) }Procedures are first-class. Variadic: proc(args: ..int). Default args, named call sites (add(a=1, b=2)).
Strings: Immutable byte slices interpreted as UTF-8. len(s) = bytes. Iterate runes:
for r in s { ... } // r is rune; UTF-8 decoded automaticallyString concatenation with + allocates via the context allocator. Use strings.builder for efficient building.
Collections:
- Fixed array:
arr: [4]int = {1,2,3,4} - Slice:
s: []int = arr[:] - Dynamic:
xs: [dynamic]int; append(&xs, 1); delete(xs) - Map:
m: map[string]int; m["a"] = 1; delete(m)
You must delete() dynamic containers — there is no GC.
Intermediate
Type system depth:
-
Distinct types:
Meters :: distinct f32— no implicit conversion tof32 -
Tagged unions:
Result :: union { int, string, Error }with type-switch:switch v in result { case int: fmt.println("int", v) case string: fmt.println("str", v) case Error: fmt.println("err", v) } -
Enums + bit_sets:
Color :: enum {Red, Green, Blue}; mask: bit_set[Color] = {.Red, .Blue} -
Bit fields:
RGB565 :: bit_field u16 { r: u16 | 5, g: u16 | 6, b: u16 | 5 } -
Parametric polymorphism (generics): prefix type parameter with
$:Pair :: struct($T: typeid) { a, b: T } swap :: proc(a, b: $T) -> (T, T) { return b, a } -
whereclauses add compile-time constraints:add :: proc(a, b: $T) -> T where intrinsics.type_is_numeric(T) { return a + b } -
anyfor type-erased args (used byfmt.println);typeidis a runtime type token;reflect.type_info_of(T)for full RTTI
Modules: “Package” = directory. Cyclic imports are forbidden. core:fmt, core:os, core:strings, core:strconv, core:slice, core:mem, core:thread, core:sync, core:time, core:net, core:encoding/json, core:encoding/xml, core:image/png, core:crypto/*, core:math/linalg (linear algebra with built-in matrix types).
Error handling: No exceptions. The convention is multiple return with a final value being an error/ok flag:
data, err := os.read_entire_file("foo.txt")
if err != nil { ... }The or_return and or_else operators short-circuit:
data := os.read_entire_file("foo.txt") or_return // propagate err
data := os.read_entire_file("foo.txt") or_else []u8{} // defaultPanics via panic("msg"); assert(cond) for invariants.
Concurrency: core:thread for OS threads, core:sync for Mutex/RW_Mutex/Cond/Sema/Atomic, core:thread/pool for thread pools, intrinsics.atomic_* for atomics. There are no green threads, no async/await, no built-in actor model — the language is “do it yourself, but with good primitives.” Job systems are typically hand-rolled.
I/O: core:os (files, env, args, exec). Note: a NEW core:os was introduced in Oct 2025 with a cleaner API and a transition period; old core:os still works but is being phased out. core:os/os2 is the new home in some snapshots; check odin doc core:os against your version.
Stdlib highlights: core:fmt (printf-family), core:math/linalg (vec/mat with swizzling), core:image/{png,bmp,tga,qoi}, core:net (TCP/UDP/HTTP-low-level), core:encoding/{json,xml,csv,base64,hex,varint,cbor}, core:crypto/* (hashes, AEADs, including new core:crypto/noise in May 2026), core:simd, core:container/{queue,priority_queue,small_array,bit_array}.
Advanced
Memory model — the context system: Every procedure gets an implicit context: runtime.Context parameter that holds:
allocator(default heap)temp_allocator(per-thread arena, resettable)logger,assertion_failure_proc,random_generatoruser_ptr,user_indexfor app-specific data
You override locally via:
context.allocator = my_arena
defer free_all(my_arena)
data := make([]u8, 1024) // uses my_arenaCustom allocators are first-class — implement an Allocator_Proc and you have a fully-typed allocator. Built-in: mem.tracking_allocator (leak detection), mem.scratch_allocator, mem.arena_allocator, mem.dynamic_pool, mem/virtual.arena_init_* (committed memory tricks).
Concurrency deep dive: core:thread wraps OS threads with a uniform Linux/Windows/macOS API. core:thread/pool.Pool is a job system. core:sync.Atomic_* matches C++/Rust atomic ordering semantics. The runtime ships SPSC/MPMC queues in core:container. core:sync.Futex is exposed where the OS supports it. There is no async runtime — for async I/O you call epoll/io_uring/IOCP via core:sys/* directly.
FFI — foreign blocks:
foreign import "system:c"
foreign c {
@(link_name="malloc") malloc :: proc "c" (size: int) -> rawptr ---
}Calling conventions: "c", "std", "fast", "contextless" (skip context pointer — required for callbacks the OS calls). vendor: provides pre-written bindings for major libs (raylib, vulkan, sdl3, miniaudio, ENet, OpenGL, DirectX, wgpu — wgpu was bumped to v29.0.0 in dev-2026-05).
Reflection: typeid_of(T), type_info_of(T), type_info_of(T).variant — you get a tagged Type_Info describing structs/arrays/enums/etc. Used by fmt.println to print arbitrary values, by core:encoding/json for marshalling, and by users to write custom serializers.
Performance tools: Compile flags: -o:none / -o:minimal / -o:size / -o:speed / -o:aggressive. -no-bounds-check for hot loops. -disable-assert. -microarch=native. -debug for full DWARF; pair with gdb/lldb/rr/tracy (Tracy bindings exist in vendor:). core:prof/spall is the official sampling profiler. intrinsics.cpu_relax, intrinsics.prefetch_* for tight loops.
God mode
when (compile-time if): Branches that are eliminated at compile time, with full type checking on the live branch. Replaces preprocessor #ifdef:
when ODIN_OS == .Windows { import "win32" } else { import "posix" }#config and #defined: Pass -define:KEY=value on CLI; access via #config(KEY, default). #defined(KEY) checks if defined.
Parametric polymorphism with $ and where: Compile-time generics with no runtime cost. proc(arr: $T/[$N]E) -> E matches “any fixed array, capturing element type and size.”
SOA structures: #soa[N]Vec3 lays out as xs: [N]f32, ys: [N]f32, zs: [N]f32 instead of interleaved — cache-friendly for SIMD/data-oriented design. Field access stays arr[i].x syntactically; the compiler does the index math.
Custom allocators as first-class values: Pass an allocator into any function via context.allocator = my_alloc. Use mem/virtual to reserve huge address ranges and commit on demand. Combine mem.scratch_allocator (per-frame arena) with the context system for “no heap allocation in the hot path” without changing function signatures.
Foreign blocks bypass binding generators: Want libfoo? Drop a foreign import "system:foo" block with the proc declarations you need — no SWIG, no codegen.
Build system as Odin code: There IS no Makefile. People write build.odin files that os.exec the odin compiler with various flag sets. For complex multi-target builds, JangaFX and others ship build.odin programs you run with odin run build.odin -file.
Inline assembly: asm{} blocks for direct asm; intrinsics.* exposes architecture intrinsics (SIMD, atomics, prefetch, popcount, etc.).
Matrix types are first-class: m: matrix[3, 3]f32 with built-in *, transpose, inverse, determinant. Limit raised from 16 to 64 in dev-2026-05 (so matrix[8,8] now legal).
Idioms & style
- Naming:
snake_casefor vars/procs/files,Pascal_Casefor types and enum variants — note the underscored Pascal (Linked_List, notLinkedList) - Constants in
SCREAMING_SNAKEtraditionally, but many code bases usePascal_Casefor::constants - Procedure declaration style:
name :: proc(...) -> ... { ... }— neverproc name(...) - Formatter:
odin strip-semicolonremoves legal-but-discouraged semicolons. There is no canonical formatter as opinionated as gofmt;ols(the LSP) provides best-effort formatting; community uses ols + format-on-save. Style guide: https://github.com/odin-lang/Odin/wiki/Naming-Convention - Idiomatic patterns: explicit over implicit, allocator-aware (always document who allocates), prefer slices over pointers, return errors as the last return value, use
or_returnaggressively, usedeferto pair acquire/release, usecontext.temp_allocatorfor short-lived scratch memory thenfree_all(context.temp_allocator) - Expert review focus: allocator hygiene (every
make/newhas matchingdelete/free, or uses an arena/temp allocator),contextmodifications scoped viadefer,vendor:use over hand-rolled bindings,whereclauses on generics for clear errors, no use ofcstringoutside FFI surfaces
Ecosystem
| Domain | Library |
|---|---|
| Game/graphics | vendor:raylib, vendor:sdl2/sdl3, vendor:OpenGL, vendor:vulkan, vendor:wgpu, vendor:directx/*, vendor:glfw |
| Audio | vendor:miniaudio, vendor:ENet |
| Image/text | vendor:stb/image, vendor:stb/truetype, vendor:stb/rect_pack |
| GUI | vendor:microui, ImGui via community bindings |
| Linear algebra | core:math/linalg (built-in vec/mat/quat) |
| Web/HTTP | core:net (low level); third-party odin-http |
| Test | Built-in @(test) + core:testing; odin test . |
| Docs | odin doc, hosted at https://pkg.odin-lang.org/ |
| LSP/IDE | ols (Odin Language Server), Odin extensions for VS Code/Sublime/Vim |
Notable users: JangaFX (EmberGen, GeoGen, LiquiGen — commercial VFX software entirely in Odin), Hat in Time team experiments, indie game studios. Bill Hall full-time on Odin since ~2020.
Gotchas
- No GC, no destructors —
delete()/free()/defer free_all()are your job. Usemem.tracking_allocatorin dev to find leaks contextis implicit — easy to forget. Procedures called from C callbacks need"contextless"calling convention OR they will read uninitialized context memory and crashtemp_allocatoris per-thread and never auto-freed — callfree_all(context.temp_allocator)per frame/per request, or it grows unbounded- String slicing is byte-based — slicing mid-codepoint produces invalid UTF-8
- Maps and dynamic arrays are NOT zero-cost movable — they own heap; copying the value copies the header but aliases the heap, leading to double-free
- No central package registry — dep management is “vendor it” / Git submodule. Some use
git-subrepo; others use ad-hoc scripts core:osis in transition — the newcore:os(andcore:os/os2) is replacing the old; some tutorials reference deprecated APIs. Check release notes for your dev-YYYY-MM- Parametric polymorphism error messages are improving but can still be cryptic when constraints fail.
whereclauses help #soacontainers do NOT iterate asfor v in arr— you iterate by index and access fields. Some idioms differ from regular arrays- Windows builds need a C compiler available — Odin invokes
link.exe(MSVC) orlldfor the final link step - No 1.0 promise — APIs in
core:may rename/move between monthly releases. Subscribe to https://odin-lang.org/news/ for newsletters
Citations
- Odin official site: https://odin-lang.org/
- Overview: https://odin-lang.org/docs/overview/
- News (releases + newsletters): https://odin-lang.org/news/
- 2025 Q4 / 2026 Q1 newsletter: https://odin-lang.org/news/2025q4-2026q1-newsletter/
- Latest release dev-2026-05 (2026-05-04): https://github.com/odin-lang/Odin/releases/tag/dev-2026-05
- Source: https://github.com/odin-lang/Odin
- Package docs: https://pkg.odin-lang.org/
- Naming convention: https://github.com/odin-lang/Odin/wiki/Naming-Convention
- New
core:osrationale (2025-10-31): https://odin-lang.org/news/transitioning-to-new-core-os/ - Orca platform announcement (2024-12-11): https://odin-lang.org/news/orca-platform/