This is an experimental fork of the Odin Programming language that adds Methods and somewhat smart dispatch. It doesn't use vtables; instead it implements dispatch as switch statements. Definitely not in line with Odin design principles and not intended to ever be merged or even used by anyone but me.
That said, pretty cute!
Implemented by Claude (Opus 4.7). I haven't looked at a single line of the code.
Companion language server fork: Methodin-ols (a fork of DanielGavin/ols that understands the new syntax).
package main
import "core:fmt"
Circle :: struct {
r: f32,
area :: proc() -> f32 {
return 3.14159265 * r * r
},
}
Rectangle :: struct {
w, h: f32,
area :: proc() -> f32 {
return w * h
},
}
Triangle :: struct {
base, height: f32,
area :: proc() -> f32 {
return 0.5 * base * height
},
}
Shape :: union {
Circle,
Rectangle,
Triangle,
}
main :: proc() {
shapes := []Shape{
Circle{r = 2},
Rectangle{w = 3, h = 4},
Triangle{base = 5, height = 6},
}
for &s in shapes {
fmt.printfln("%v -> area = %.2f", s, s.area())
}
}Each variant declares its own area inline. The compiler lifts each to a
free proc (Circle__area, Rectangle__area, Triangle__area), synthesises
a procedure group at file scope, and expands s.area() on the union to a
switch v in s over the known variants. No vtable, no indirection.
package main
import "core:fmt"
Animal :: struct {
name: string,
introduce :: proc() {
fmt.printfln("Hi, I'm %s.", name)
},
}
Dog :: struct {
using animal: Animal,
}
impl Dog {
speak :: proc() {
fmt.printfln("%s: woof!", name)
}
}
Cat :: struct {
using animal: Animal,
}
impl Cat {
speak :: proc() {
fmt.printfln("%s: meow.", name)
}
}
Pet :: union { Dog, Cat }
main :: proc() {
rex := Dog{animal = {name = "Rex"}}
mittens := Cat{animal = {name = "Mittens"}}
rex.introduce() // inherited from Animal via `using`
mittens.introduce()
pets := []Pet{rex, mittens}
for &p in pets {
p.speak() // union-dispatched: Dog__speak vs Cat__speak
}
}Dog and Cat don't redeclare introduce — they inherit it by embedding
Animal with using, and the UFCS resolver walks the embed chain to find
it. speak is declared in an impl block, which is exactly the same
lifting a
8000
s an in-struct proc — useful when you want the method to live in
a different file from the struct.
See examples/methods and
examples/animals for the runnable demos.
-
UFCS —
x.foo(args)desugars tofoo(x, args)whenfoois a free proc in the receiver type's package. The compiler auto-takes the receiver's address when the parameter is a pointer and the receiver is addressable. Field selectors always win over UFCS, so there is no ambiguity. Resolution walksusingchains, including across packages. -
In-struct proc declarations — methods can be declared directly inside a struct body. Each is lifted to a free
name :: proc(using self: ^Struct, ...) { ... }at package scope, so the body reads fields without aself.prefix. -
impl Type { ... }blocks — same desugaring as in-struct procs, but declared in a block separate from the struct, optionally in another file. -
Auto procedure groups on name collisions — when two structs in the same package declare a method with the same name, the compiler mangles each lifted proc (
Knight__hit,Wizard__hit) and synthesises a procedure group (hit :: proc { Knight__hit, Wizard__hit }) at file scope. UFCS dispatches via overload resolution on the receiver type. -
Union method dispatch — calling a method on a tagged union expands to a
switch v in sover the known variants at compile time. No vtable, no indirection. Variadic methods forward correctly, and calling a method on a nil union value panics with the union and method name instead of silently doing nothing. -
auto_union(T)— a tagged union of every struct in the program thatusing-embedsTat offset 0 (directly or transitively). Bind it to a named alias (AnyEntity :: auto_union(Entity)) and method calls on it virtually dispatch to each variant's override; base fields are promoted through the offset-0 layout. Cross-package variants work. Seeexamples/auto_union_test. -
Virtual sibling dispatch — a method that calls a sibling method gets a polymorphic
^$Selfreceiver, so each concrete receiver re-resolves the call against its own overrides (virtual dispatch, still no vtable). Seeexamples/self_dispatch. -
Type-scoped constants — any non-proc
Name :: <expr>inside a struct body orimplblock is a constant (or nested type alias) accessed asVec3.UP,World.MAX_ENTITIES,World.Id— usable in constant contexts like array sizes.implworks on non-struct named types too, soimpl Vec3 { UP :: Vec3{0, 1, 0} }works on adistinct [3]f32. -
Explicit receivers in
implblocks — an impl method whose first parameter is namedselfkeeps that explicit receiver and nothing is injected (scaled :: proc(self: Vec3, f: f32)); every other method getsselfinjected, non-struct targets included. The receiver is matched by name, never by type:lerp :: proc(to: Color, t: f32)insideimpl Colortakestoas an ordinary operand and still hasselfavailable. -
Methods win over free procs in UFCS — when
x.lerp(...)matches both a lifted method (lerpinimpl Color) and imported free procs that accept the receiver as a first argument (math.lerp,linalg.lerp), the method is chosen and the free procs are dropped, mirroring inherent-beats-extension resolution in other languages. Only genuine method/method or free/free ties report an ambiguity. -
Rvalue receivers — methods work on temporaries: function results (
make_world().describe()), constants (Vec3.UP.scaled(2)), struct literals, and immutable parameters. A hidden local is materialized for the^selfreceiver — but ONLY when the checker proves the method never mutates its receiver. Calling a mutating method on a temporary is a compile error that names the exact write that would be lost:Error: Cannot call 'take_damage' on 'make_world()', which is a temporary value: the method mutates its receiver and the mutation would be lost 'hp' is written at world.odin(25:3) Suggestion: store the value in a variable first, then call the method on itThe analysis is transitive through sibling calls and conservative: address-taken fields, slices of fields, passing
selfonward, and anything it can't prove read-only count as mutating. -
Default struct field values —
hp: int = 100in a struct body, for any constant-expressible value (numbers, strings, compound literals, nested structs, fixed arrays). Applied wherever the compiler initializes the type: declarations without an initializer, compound literals that omit the field, globals, and arrays. Heap memory keeps zero semantics (new/makenever run defaults — a default is a constant, never code);ptr^ = {}applies them explicitly. Seeexamples/type_members.
Nothing here is dynamic dispatch through function pointers — every call resolves statically to a known procedure (or one variant of a procedure group / one case of a compile-time switch), and the inliner sees through it.
odin watch builds and runs a package, then live-reloads its code when the
source changes — without restarting the program and without losing global
state.
odin watch examples/hot_reloadWhile it runs, edit a procedure body and save; the running program
immediately starts using the new code, and globals keep their values. No
special program structure or contract is required — an ordinary
package main with a normal main just works.
How it works (all guarded so normal builds are unaffected):
- In a
watchbuild the compiler routes every call to a package-level procedure through a writable dispatch slot (a function pointer) instead of a direct call, and exports package globals as dynamic symbols. - On a source change an embedded reload agent rebuilds the package as a shared
library whose package globals are imported from the running host,
dlopens it, and overwrites each dispatch slot with the new procedure address. Because globals are never reloaded, program state is preserved automatically.
An optional hot_reloaded :: proc() in your package is called once after each
successful reload — handy for re-deriving cached state. Omit it and reloads
happen silently.
Some edits can't be patched into a running native process. The agent detects these via a compiler-emitted structural signature and automatically restarts (losing in-memory state) instead of corrupting it:
- editing
main's body (the running loop can't be re-entered); - adding, removing, or changing the type of a package global;
- editing the layout of any type declared in the package (fields, offsets, sizes — live globals and heap objects hold the old layout).
Everything else — editing any other procedure's body — hot-reloads in place.
The restart is handled by the parent odin watch process re-exec'ing
itself, so the process tree stays flat no matter how many restarts happen.
Odds and ends:
ODIN_HOT_RELOADis a built-inboolconstant —when ODIN_HOT_RELOADgates code to hot-reload builds (true in the host and in reload libraries).- Reload rebuilds inherit the host's
-define:flags,-debug, and checker threading, so each per-edit rebuild matches the original build. - Both directory and single-file (
-file) packages work, on Linux, macOS, and Windows. The whole project tree is watched: subpackages in directories beneath the watched root (e.g. an engine package at<project>/engine) are rebuilt into each reload, and their globals are shared with the host just like the root package's, so engine state survives reloads too. Out-of-tree dependencies (base:/core:/vendor:) are not watched and keep per-reload private copies. Rude-edit detection (globals/type layouts) covers every watched package; proc bodies — including procs declared inside structs — reload in place. Seeexamples/hot_reloadfor a runnable demo.
The checker runs multithreaded by default (matching upstream); pass
-no-threaded-checker to serialize it if you ever suspect a
checker race. Methodin's polymorphic method machinery serializes its own
shared-state binding internally — everything else checks in parallel.
The Data-Oriented Language for Sane Software Development.
Odin is a general-purpose programming language with distinct typing, built for high performance, modern systems, and built-in data-oriented data types. The Odin Programming Language, the C alternative for the joy of programming.
Website: https://odin-lang.org/
package main
import "core:fmt"
main :: proc() {
program := "+ + * 😃 - /"
accumulator := 0
for token in program {
switch token {
case '+': accumulator += 1
case '-': accumulator -= 1
case '*': accumulator *= 2
case '/': accumulator /= 2
case '😃': accumulator *= accumulator
case: // Ignore everything else
}
}
fmt.printf("The program \"%s\" calculates the value %d\n",
program, accumulator)
}
Instructions for downloading and installing the Odin compiler and libraries.
Get the latest nightly builds of Odin.
An overview of the Odin programming language.
Answers to common questions about Odin.
Documentation for all the official packages part of the core and vendor library collections.
Examples on how to write idiomatic Odin code. Shows how to accomplish specific tasks in Odin, as well as how to use packages from core and vendor.
Documentation for the Odin language itself.
Get live support and talk with other Odin programmers on the Odin Discord.
The official blog of the Odin programming language, featuring announcements, news, and in-depth articles by the Odin team and guests.