Back to blog

Why I Write Odin

the language i was looking for

i've been writing systems code for a while now. i've done the Rust thing, the Go thing, the C thing. they're all fine. they all have their place. but when i found Odin, something clicked in a way that hadn't before. it felt like someone had taken all the things i actually cared about in a systems language and stripped away everything i didn't.

i've since built three projects in it — Pegasus (a PEG parser generator), Wayu (a TUI for managing shell configs), and bld (a build tool). each one reinforced the same feeling: this is the language i want to think in.

let me explain why.

data-oriented design as a first-class citizen

most languages start with behavior. you define classes, attach methods, build inheritance trees. then you figure out where the data goes. Odin starts the other way around. you think about your data first — what it looks like in memory, how it's laid out, how the CPU is going to access it.

structs of arrays instead of arrays of structs. cache-friendly layouts by default. no vtables hiding behind your polymorphism. when i'm writing a parser that needs to chew through thousands of nodes, i want to know exactly how those nodes sit in memory. Odin makes that the natural way to think.

// data-oriented: parallel arrays, cache-friendly iteration
Particle_System :: struct {
    positions:  [MAX]Vec3,
    velocities: [MAX]Vec3,
    lifetimes:  [MAX]f32,
    count:      int,
}

// tight loop over contiguous memory — no pointer chasing
tick :: proc(ps: ^Particle_System, dt: f32) {
    for i in 0..<ps.count {
        ps.positions[i] += ps.velocities[i] * dt
        ps.lifetimes[i] -= dt
    }
}

no indirection. no virtual dispatch. just data and the code that transforms it.

no hidden control flow

this is the big one for me. in Odin, what you see is what runs. there are no constructors that fire when you declare a variable. no destructors that run when something goes out of scope. no operator overloading that turns a + b into a function call you didn't write. no exceptions that teleport you up the stack.

// no hidden control flow — what you see is what runs
process :: proc(items: []Item, allocator := context.allocator) -> []Result {
    results := make([]Result, len(items), allocator)
    for item, i in items {
        results[i] = transform(item)
    }
    return results
}

that procedure does exactly what it says. make allocates. the loop transforms. it returns. there's no implicit Drop trait running cleanup. there's no exception handler wrapping the call. if something fails, you'll see the error handling right there in the code because Odin uses explicit error returns.

after years of C++ where a single line of code could trigger a cascade of constructors, copy operators, and destructors — this is genuinely refreshing. i can read a function and know what it does.

manual memory without the pain

this is where people expect me to say "just use Rust." and look, Rust's borrow checker is an engineering marvel. but it's also a second language you have to learn on top of the actual language. half my time in Rust was fighting the compiler about lifetimes for code that i knew was correct.

Odin takes a different approach. you manage memory manually, but the language gives you tools that make it painless:

arena allocators — instead of tracking every individual allocation, you group things by lifetime and free them all at once. Pegasus has two arenas: one for the grammar that lives forever, one for per-parse scratch space that gets nuked after each run.

the context system — every procedure in Odin has an implicit context parameter that carries the current allocator (among other things). you can swap it out at any scope boundary. no need to thread allocator parameters through every function call manually.

defer — simple, predictable cleanup. no RAII magic, just "run this when the scope exits."

// defer for cleanup — simple, predictable
load_config :: proc(path: string) -> (Config, Error) {
    f := os.open(path) or_return
    defer os.close(f)

    data := os.read_entire_file(f) or_return
    defer delete(data)

    return parse_config(data)
}

you can see every allocation and every cleanup. nothing is hidden. nothing is implicit. and yet it's not the footgun parade that C gives you, because defer and arenas handle 90% of the cases where C programmers mess up.

the joy of a simple language

the entire Odin spec fits in your head. i'm not exaggerating. after a week i stopped looking things up. compilation is instant — like, actually instant, not "fast for a compiled language" instant. error messages are clear and point at the actual problem.

compare this to Rust where the language has traits, lifetimes, async, macros, const generics, GATs, and a new feature every six weeks. or Go where the language is simple but the opinions are suffocating — you will format your code this way, you will handle errors this way, and also here's a garbage collector you can't turn off. or C where the language is simple but the footguns are everywhere and the tooling is from 1983.

Odin sits in this sweet spot. simple enough to hold in your head. powerful enough to build real things. honest enough to show you what's actually happening.

i'm not saying it's perfect. the ecosystem is small. the community is niche. you're not going to find a crate or a package for everything. but for the kind of work i do — parsers, build tools, TUIs — that's fine. i'd rather write the code myself in a language i enjoy than glue together dependencies in a language that fights me.

that's why i write Odin.