You are translating one Rust file to Odin. Read this whole document before
writing any code. The goal of Phase A is a draft .odin next to the
.rs that captures the logic faithfully — it does not need to compile.
Phase B makes it compile package-by-package.
This guide is project-agnostic. If you're porting a specific codebase, fill in the "Package map" table below with your crate→package layout before starting — everything else applies generally.
- Write the
.odinin the same directory as the.rs, same basename (socket.rs→socket.odin), unless that directory isn't yet an Odin package (no other.odinfiles) — in that case this file becomes the package's entry file and should be named after the package (lib.rs/mod.rs→<package_name>.odin). - One Odin package per directory, same as Rust's one crate/module-tree per directory. Don't invent sub-packages Rust didn't have as separate modules.
- No async runtime, no macro system, no trait objects exist in Odin. Where the Rust leans on any of these, see the dedicated sections below — don't invent ad-hoc workarounds before reading them.
- Match the Rust's structure. Same fn names (snake_case in both
languages — this carries over almost for free), same field order, same
control flow. Phase B reviewers diff
.rs↔.odinside-by-side. Type names stayPascal_Case-ish per Odin convention (HttpClient→Http_Client) — apply this renaming mechanically and consistently, don't leave a mix. - Leave
// TODO(port): <reason>for anything you can't translate confidently. Don't guess. Flagging is better than wrong code. This is especially important for: macro-generated code, trait-object dispatch, async state machines, and anything relying on the borrow checker for correctness rather than just compile-time bookkeeping. - Leave
// PERF(port): <rust idiom> — profile in Phase Bwherever the Rust used a perf-specific idiom (with_capacity,Cow, SIMD intrinsics,#[inline(always)], arena allocation via a crate likebumpalo) and the port uses the plain idiomatic Odin form. - Odin has no borrow checker. This is not a license to be sloppy — it
means the compiler will not catch use-after-free, double-free, or aliased
mutation that Rust's borrow checker was silently preventing. Anywhere the
Rust code's correctness depended on
&mutexclusivity (not just on compiling), leave// TODO(port): verify aliasing — was &mut exclusive in Rust. - Odin has no move semantics. Assigning a struct, passing it by value,
or returning it copies it (shallow copy of the struct's fields,
including any pointer/slice/dynamic-array headers inside it). Rust's
let y = x;movingx(and making further use ofxa compile error) has no Odin analogue —y := xin Odin just duplicatesx, and the originalxremains valid and usable. Where the Rust's logic depends on the old binding being dead (e.g. aVec/String/Boxwhose buffer is now solely owned byy), the Odin port will have two headers pointing at the same backing buffer — this is the single most common silent bug in this kind of port. Leave// TODO(port): was a move — verify no aliasingat every such site; do not silently assume copy semantics are equivalent. unsafe {}has no Odin equivalent block — Odin doesn't gate raw pointer dereferences, casts, or FFI calls behind a keyword the way Rust does. Where Rust marked somethingunsafe, keep a// SAFETY: <why>comment mirroring the Rust invariant anyway, even though Odin won't enforce anything — it's load-bearing documentation for Phase B reviewers.- Prereq for every package: if the Rust used a non-default global
allocator (
mimalloc,jemalloc, a custom#[global_allocator]), set the equivalent viacontext.allocatorat the program's entry point (main) usingcontext = runtime.default_context()overridden with your allocator, orcontext.allocator = my_allocator()— otherwise everynew/makein this file silently uses Odin's default heap allocator instead. Phase B wires this; Phase A can assume it's already done.
use crate_name::module::Thing → figure out which Rust crate crate_name
is, then map it to an Odin package import. If you're porting a specific
project, replace this table with your real crate→package layout:
| Rust | Odin |
|---|---|
crate_name::module::Thing |
import "path/to/crate_name" then module.Thing if module is a sub-package, or crate_name.Thing if it was a flat module re-exported at crate root |
external crate (crates.io) with no project Odin equivalent |
vendor it under vendor:<name> if a maintained Odin port exists, otherwise write the minimal subset needed directly into a _sys-style FFI package (see §FFI) and leave // TODO(port): no vendor equivalent for <crate> |
mod foo; / mod foo { ... } (file-private submodule) |
a subdirectory package foo/, or keep in the same file if the Rust module was just visibility scoping with no real separation — Odin has no inline mod {} blocks, only files-as-packages |
pub use re-export |
a thin re-export isn't idiomatic Odin (no pub use equivalent); callers import the defining package directly. Update call sites in Phase B rather than faking the re-export. |
Cargo.toml dependency declaration |
nothing 1:1 — Odin has no package manager; dependencies are either vendored under vendor:, fetched as a git submodule, or hand-copied into the tree. Phase B resolves this. |
If a Rust path doesn't fit the above: the Odin package is named after the
directory the Rust module lives in, snake_case, no crate_ prefix
(Odin packages are just directory names — don't invent a naming
convention Rust didn't need).
| Rust | Odin | notes |
|---|---|---|
i8/i16/i32/i64/i128/isize |
i8/i16/i32/i64/i128/int |
identical names except isize→int (Odin's int is pointer-sized, matching isize, not i32) |
u8/u16/u32/u64/u128/usize |
u8/u16/u32/u64/u128/uint |
same caveat as above |
f32/f64 |
f32/f64 |
1:1 |
bool |
bool |
1:1 |
char |
rune |
both are a single Unicode scalar value (4 bytes); Odin's rune is exactly Rust's char, not a single byte |
&str |
string |
Odin string is a read-only {data: ^u8, len: int} view, same semantics as &str. Caveat: Odin's string is not guaranteed-UTF-8 by the type system the way Rust's str is guaranteed by the compiler — Odin only promises it by convention. If the Rust leaned on str's UTF-8 guarantee for safety (not just correctness), leave // TODO(port): Rust guaranteed UTF-8 here, Odin does not. |
String |
string for an immutable final value; strings.Builder (import "core:strings") while under construction |
Odin has no separate owned/growable string type — build with a Builder, then strings.to_string(builder) to get the final string. Don't reach for [dynamic]u8 unless the Rust was genuinely manipulating raw bytes, not text. |
&[u8] / Vec<u8> (byte buffer, not text) |
[]u8 / [dynamic]u8 |
|
&[T] |
[]T |
Caveat: Odin slices don't distinguish mutable vs immutable the way &[T] vs &mut [T] do — there is one []T and whether callers mutate through it is a convention, not a type-level guarantee. If the Rust function signature took &[T] specifically to promise it wouldn't mutate, that promise is now unenforced; note it in a doc comment, don't silently drop it. |
&mut [T] |
[]T |
same type as above — see caveat |
[T; N] |
[N]T |
fixed-size array, 1:1 (note Odin puts the size first) |
Vec<T> |
[dynamic]T |
.push(x) → append(&v, x) · .pop() → pop(&v) · .len() → len(v) · v[i] → v[i] · .clear() → clear(&v) · .remove(i) → ordered_remove(&v, i) (core:slice) · .swap_remove(i) → unordered_remove(&v, i) · drop/free → delete(v) |
Box<T> |
^T |
heap-allocate with new(T) (zero-initialized) or new_clone(value) (copies an existing value onto the heap); free with free(ptr). No automatic drop — see §Ownership. |
Box<[T]> |
[]T (heap-backed, owned) |
allocate via make([]T, n); free via delete(slice). Odin doesn't distinguish an owned heap slice from a borrowed one at the type level — track ownership by convention/comment. |
Rc<T> / Arc<T> |
no built-in equivalent | hand-roll: struct { value: T, ref_count: int } behind a ^T, with explicit _retain/_release procs, or — if single-threaded and the Rust never relied on weak refs — just pass ^T around and let one clear owner free it. Leave // TODO(port): Rc/Arc — verify single owner or hand-roll refcounting. Arc's atomicity → core:sync atomics on the count field. |
Weak<T> |
no built-in equivalent | keep a raw ^T (or an index into a stable slot, e.g. a slot map) and a manual "still alive" check (tombstone bit, generation counter); Odin has no weak-pointer primitive |
RefCell<T> / Cell<T> |
plain T behind a ^T |
Odin has no compile-time borrow checking to defer to runtime in the first place — RefCell's runtime borrow tracking existed only to work around the borrow checker, which Odin doesn't have. Just use the value directly. If the Rust's RefCell panics were load-bearing for catching real aliasing bugs (not just appeasing the borrow checker), keep a manual in_use: bool guard and // TODO(port): was RefCell — verify no real aliasing bug masked here. |
Mutex<T> / RwLock<T> |
sync.Mutex / sync.RWMutex (core:sync) wrapping the value, or a ^T guarded by one |
sync.mutex_lock(&m) / sync.mutex_unlock(&m); no lock-guard-as-RAII, so pair every lock with an explicit defer sync.mutex_unlock(&m) |
Option<T> |
multi-return (T, bool) from the producing call, or a tagged union Maybe :: union { T } (nil-state union) if T must be stored as a field |
Odin's idiom is "value + ok bool" at call sites (v, ok := m[k]), not a boxed Option type living in a struct field. For a struct field that's genuinely optional, a union { T } works (an unset union is nil) — but only if T isn't itself a union. |
Result<T, E> |
multi-return (T, Error) |
see §Error handling — this is a dedicated section, don't improvise here |
() (unit) |
no return value (omit the -> clause) |
|
! (never type) |
no equivalent; the proc simply has no path that returns | |
(A, B, C) (tuple) |
multi-return (A, B, C) from a proc, or an anonymous-feeling small struct { a: A, b: B, c: C } for a stored value |
Odin has no tuple type usable as a value/field — only multiple return values from a call. A tuple stored in a struct field needs a named struct. |
struct Foo { x: i32 } |
Foo :: struct { x: i32 } |
|
struct Foo(i32) (newtype) |
Foo :: distinct i32 |
Odin's distinct is exactly Rust's single-field newtype pattern — a new nominal type with the same layout, no implicit conversion |
enum Foo { A, B, C } (no data) |
Foo :: enum { A, B, C } |
1:1 |
enum Foo { A(i32), B(String), C } (data-carrying) |
tagged union: define one struct per data-carrying variant, then Foo :: union { A, B, C } where C (no data) can be its own empty struct{} marker type or represented by the union's nil state if it's the only empty variant |
Odin enums are C-like (name→integer only, no payload); a Rust sum type with payloads is always a union, never an enum, in the port |
match on the enum above |
switch v in foo_union { case A: ...; case B: ...; case C: ... } |
type-switch on the union; Odin checks this for exhaustiveness by default (compile error if a variant is unhandled) — add #partial switch only if intentionally incomplete, mirroring Rust's _ => {} |
trait Foo { fn bar(&self); } + impl Foo for T |
see §Traits — no 1:1, dedicated section | |
dyn Foo / Box<dyn Foo> |
see §Traits | |
impl Trait (generic bound) |
parametric proc with a where clause, see §Generics |
|
fn foo<T: Trait>(x: T) |
foo :: proc(x: $T) where <constraint> |
see §Generics |
closures (Fn/FnMut/FnOnce, |x| ...) |
proc(...) -> ... value, or a struct holding captured state + an explicit call proc |
see §Closures |
&'a T (borrow with lifetime) |
^T (or T passed by value if small and Copy) |
drop the lifetime entirely — Odin pointers carry no lifetime, see §Ownership for the implication |
PhantomData<T> |
drop entirely, no field needed | Odin generics don't need a marker field to "use" a type parameter |
HashMap<K, V> |
map[K]V |
built into the language; m[k] = v, v, ok := m[k], delete_key(&m, k), iterate with for k, v in m |
HashSet<T> |
map[T]bool or map[T]struct{} |
no built-in set type; a map[T]bool (membership = value true) is the common idiom |
BTreeMap<K, V> / BTreeSet<T> |
no built-in sorted map | maintain a sorted [dynamic]K alongside a map[K]V and binary-search it (core:slice.binary_search), or keep entries in a sorted [dynamic]struct{key:K, val:V}. Flag // TODO(port): was BTreeMap, verify ordering-dependent logic since this is easy to get subtly wrong. |
VecDeque<T> |
no built-in ring buffer | hand-roll a ring buffer over [dynamic]T (or a fixed [N]T if bounded), or use two stacks if only push/pop at both ends is needed. Flag // TODO(port): was VecDeque. |
BinaryHeap<T> |
no built-in heap | core:container/priority_queue provides one — use it rather than hand-rolling |
LinkedList<T> |
intrusive linked list by hand (raw ^Node + manual next/prev fields) |
Odin has no built-in linked list; if the Rust used LinkedList for genuine O(1) splice/insert reasons, port the node struct directly. If it was just used as a stack/queue, prefer [dynamic]T. |
Cow<'a, T> |
drop the Cow; decide up front whether the function takes an owned T or a borrowed ^T/T and write that. If the original truly needed both paths, keep two code paths with a bool owned flag. |
leave // PERF(port): was Cow — may now always-copy or always-borrow |
*const T / *mut T |
^T |
Odin pointers are not const-qualified — same caveat as &[T] vs &mut [T] above |
*const c_void / *mut c_void |
rawptr |
|
NonNull<T> |
^T |
Odin pointers can be nil; if the Rust's NonNull was load-bearing for a safety invariant (not just an optimization), keep an assert at construction sites: assert(ptr != nil) |
#[repr(C)] struct |
a plain Odin struct |
Odin's default struct layout already matches the platform C ABI — no annotation needed unless you also need #[repr(packed)], see next row |
#[repr(packed)] / #[repr(align(N))] |
#[packed] struct, or #[align(N)] on the type |
|
#[derive(Debug)] |
nothing needed | fmt.println/fmt.printf("%v", x) (core:fmt) prints arbitrary struct fields via reflection out of the box — there is no derive step |
#[derive(PartialEq, Eq)] |
nothing needed, if every field is itself comparable | Odin structs support ==/!= structurally by default as long as all fields do (numeric types, bool, fixed arrays, other comparable structs). [dynamic]T, map[K]V, and string-of-different-backing-pointer are not structurally comparable the way Rust's derived PartialEq would compare contents — string == string does compare contents (it's a byte-slice compare), but [dynamic]T == [dynamic]T will not compile. Write an explicit equal :: proc(a, b: Foo) -> bool if the struct contains a dynamic array/map field. |
#[derive(Clone)] / Copy |
usually nothing needed for the assignment itself (Odin copies structs on assignment by default) | but see the move-semantics ground rule above: a "clone" of a struct containing a [dynamic]T/map/^T field only copies the header/pointer, not the backing storage — write an explicit clone :: proc(f: Foo) -> Foo that allocates fresh backing storage for any such field if the Rust's Clone was a deep clone |
#[derive(Default)] / impl Default |
usually nothing needed | Odin's zero value is every type's default (Foo{} is fully zeroed) — only write an explicit make_foo :: proc() -> Foo { ... } if the Rust's Default set non-zero values |
impl Drop for T |
explicit destroy :: proc(t: ^Foo) (or _delete), called via defer destroy(&foo) at every construction site |
see §Ownership — no automatic destructors exist |
std::mem::size_of::<T>() |
size_of(T) |
|
std::mem::swap(&mut a, &mut b) |
core:mem's mem.swap(&a, &b), or by hand: tmp := a; a = b; b = tmp |
|
std::mem::take(&mut x) |
tmp := x; x = {} (replace with zero value) |
no built-in take, but the pattern is two lines |
bit flags (bitflags! macro) |
bit_set[Enum] |
Odin's bit_set is a first-class language feature, not a crate — define a plain enum of flag names and wrap it: Flags :: bit_set[Flag] |
| Rust | Odin |
|---|---|
let x = expr; |
x := expr |
let mut x = expr; |
x := expr — Odin has no immutable-by-default local bindings; every local declared with := is mutable. Rust's mut/no-mut distinction simply has no Odin counterpart; don't try to encode it (e.g. via a naming convention) — it adds noise Phase B will just remove. |
let x: T = expr; (explicit type) |
x: T = expr |
const X: T = expr; (true compile-time constant) |
X :: expr |
static X: T = expr; (single runtime instance) |
X: T = expr at package scope (a package-level :=/: T = is a single global, not re-evaluated per use) |
fn foo(x: i32) -> i32 { ... } |
foo :: proc(x: i32) -> i32 { ... } |
struct Foo { x: i32 } + impl Foo { fn bar(&self) -> i32 { self.x } } |
Foo :: struct { x: i32 } and a free proc foo_bar :: proc(self: ^Foo) -> i32 { return self.x }. Odin has no method-call syntax — there is no foo.bar() sugar for user types. Name the proc <Type>_<method> and call it explicitly: foo_bar(&foo). Don't try to fake methods with using; reserve using for genuine struct-embedding/composition (see next row). |
struct composition via a wrapped field accessed through Deref/DerefMut |
using field embedding: Outer :: struct { using inner: Inner, extra: i32 } — outer.inner_field becomes directly accessible as outer.inner_field without the .inner hop |
match x { A => ..., B(v) => ..., _ => ... } |
switch v in x { case A: ...; case B: ...; case: ... } (type-switch on a union) or switch x { case .A: ...; case .B: ...; case: ... } (value-switch on a plain enum) |
if let Some(x) = opt { ... } |
if x, ok := maybe_call(); ok { ... } |
while let Some(x) = it.next() { ... } |
for x, ok := it_next(&it); ok; x, ok = it_next(&it) { ... }, or restructure as a for ... in loop if iterating a slice/map directly — see §Closures & iterators |
for x in iter { ... } over a Vec/slice |
for x in slice { ... } |
for (i, x) in iter.enumerate() { ... } |
for x, i in slice { ... } |
for (k, v) in map.iter() { ... } |
for k, v in map { ... } |
loop { ... break; ... } |
for { ... break ... } |
? operator (error propagation) |
or_return |
.unwrap() |
assert(ok) followed by using the value, or for a Result-shaped call: panic with the error message if present. Never silently discard the failure path the way a careless _ = would — .unwrap()'s whole point was "crash loudly if this fails," so the port should crash loudly too. |
.expect("msg") |
assert(ok, "msg") |
.unwrap_or(default) |
v, ok := call(); v = ok ? v : default or fold into a small helper |
.unwrap_or_else(|| ...) |
same, with the fallback as an explicit expression/call instead of a closure |
panic!("msg") |
panic("msg") |
assert!(cond) / debug_assert!(cond) |
assert(cond) — Odin's assert is stripped in -define:ODIN_DISABLE_ASSERT=true release builds, mirroring debug_assert!'s debug-only nature closely enough that both Rust forms can map to the same Odin call |
unreachable!() |
panic("unreachable") |
todo!() / unimplemented!() |
panic("TODO") plus a // TODO(port): comment — don't leave a bare panic with no tracking comment |
x as i32 (numeric cast) |
i32(x) or cast(i32)x |
T::try_from(x) (checked numeric conversion) |
manual range check + cast: if x >= MIN && x <= MAX { ok_value := T(x) } else { /* handle */ } |
.into() / T::from(x) (widening, infallible) |
T(x) |
mod tests { #[test] fn ... } |
@(test) test_foo :: proc(t: ^testing.T) { ... } (import "core:testing"), run via odin test . |
#[cfg(test)] |
no direct gate needed — @(test)-attributed procs are simply excluded from non-odin test builds automatically |
dbg!(x) |
fmt.println(x) or fmt.eprintln(x) (core:fmt) |
format!("{}-{}", a, b) |
fmt.tprintf("%v-%v", a, b) (temp-allocator string, valid until next temp-allocator reset) or fmt.aprintf(...) (caller-freed, heap) |
write!(f, "...") inside impl Display |
register a custom formatter: fmt.register_user_formatter(typeid_of(Foo), proc(fi: ^fmt.Info, arg: any, verb: rune) -> bool { ... }), called once at startup |
Odin has no borrow checker and no automatic destructors. Every Drop
impl, every &/&mut exclusivity rule, and every move in the Rust source
was doing real work that the Odin port must now do by convention and
discipline instead of by compiler enforcement.
impl Drop for T { fn drop(&mut self) { ... } }→ an explicitt_destroy :: proc(t: ^T) { ... }proc, called viadefer t_destroy(&t)immediately after construction at every call site. There is no scope-exit hook — if a caller forgets thedefer, the resource leaks, silently, with no compiler warning. Flag every non-trivialDropimpl's call sites with// PORT NOTE: must defer t_destroy hereuntil Phase B has audited them all.let _guard = foo();(RAII guard pattern, e.g. aMutexGuard) → no RAII exists, so this becomesfoo_lock(&foo); defer foo_unlock(&foo)— the acquire and release are now two separate statements you must keep paired by hand. This is the single highest-risk pattern in this kind of port (areturn/breakinserted between acquire and the eventual unlock, without the matchingdefer, silently breaks the invariant). Always add thedeferon the same line as (or the line directly after) the acquire.&mut selfexclusivity (the borrow checker guaranteeing no other reference exists while this one is live) → nothing enforces this in Odin. If the Rust function's correctness (not just its ability to compile) depended on having the only reference — e.g. mutating aVecwhile an iterator over it was thought to be inert — re-verify that invariant by hand and leave// TODO(port): verify exclusivityif you're not certain.- Moving an owned value into a function/struct (
fn take(x: Vec<T>),self.field = some_box;) → in Odin this is a copy of the header (pointer/len/cap for a[dynamic]T, the raw pointer for a^T). The source variable still exists and still points at the same backing storage. If both the old and new locations might independently free or mutate that backing storage, you now have a double-free or aliased mutation that the Rust compiler was preventing for free. Resolve this at port time, don't defer it to a runtime crash in Phase B: either (a) zero out the source after the "move" (x = {}) to make the single-owner intent explicit, or (b) confirm the source is genuinely never touched again and leave a comment saying so. - Arena/bump allocation (a crate like
bumpalo, or a hand-rolled arena) → Odin has first-class arena support viacore:mem(mem.Arena,mem.arena_allocator(&arena)) — setcontext.allocatorto the arena allocator for the scope that should use it (context.allocator = mem.arena_allocator(&arena)inside a block, restored on scope exit sincecontextis itself scope-stacked), thennew(T)/make(...)inside that scope use the arena automatically. No need to thread an explicit allocator parameter through every call the way the Rust did if it wasn't using Rust'sAllocatortrait param style — Odin's implicitcontextalready does that threading for you. Pin<Box<T>>(self-referential / address-stable types) → Odin has noPin. If the type's address-stability was load-bearing (intrusive lists, callback registration storing^T), a heap-allocated^Tfromnew(T)is already address-stable for its lifetime (Odin never moves heap allocations) — thePinwrapper itself can simply be dropped, but leave// PORT NOTE: was Pin<Box<T>>, relying on heap-allocation stabilityso Phase B knows why there's a^Tinstead of a value type.
Rust's Result<T, E> + ? becomes Odin's idiomatic multi-return + or_return.
fn read_config(path: &str) -> Result<Config, ConfigError> {
let text = std::fs::read_to_string(path)?;
let config = parse(&text)?;
Ok(config)
}read_config :: proc(path: string) -> (Config, Config_Error) {
text := read_to_string(path) or_return
config := parse(text) or_return
return config, nil
}Result<T, E>→(T, Error)as the proc's return tuple, error last.- The
?operator →or_return, only valid when the enclosing proc's last return value is error-shaped (matches, or is assignable from, the failing call's error type) — same precondition?has in Rust (the error types must convert). Ok(x)at the end of a function →return x, nil(explicit, since Odin has noOk-wrapping sugar).Err(e)→return <zero-value-of-T>, e. Odin requires a value for every return slot even on the error path — use the type's zero value ({}/0/""/nil) unless a more meaningful partial value exists.- A Rust
enum AppError { NotFound, Io(io::Error), Parse(ParseError) }→ an Odin tagged union:App_Error :: union { Not_Found, IO_Error, Parse_Error }, whereNot_Foundcan be an emptystruct{}marker and the other two wrap whatever data the Rust variant carried. A function's "no error" case returns Odin'snilfor that union slot — there's noOkwrapper needed sincenilalready means "absent." .map_err(|e| ...)→ an explicitif err != nil { err = wrap(err) }(or construct the new error variant directly) — no combinator form exists, write it as a statement.thiserror's#[derive(Error)](adds aDisplayimpl +source()chaining) → register afmt.register_user_formatterfor the error union (see Idiom map'sDisplayrow) for the message half; there's no built-in error-chaining trait — ifsource()was used to walk a cause chain, add an explicitcause: ^App_Errorfield and walk it by hand.anyhow::Error/Box<dyn Error>(type-erased, heap-allocated error) → avoid reaching for an Odinany-typed error field as a reflex replacement; prefer a concrete tagged union per the row above. Only fall back toany(Odin's universal type-erased value) if the error truly needs to carry arbitrary, unbounded payload types across a boundary that can't know about them — and flag it// TODO(port): was anyhow::Error, consider a closed error union instead.or_elseis Odin's combinator for substituting a default on failure (analogous to.unwrap_or_else), not for chaining fallible calls the way Rust's.or_else(|e| ...)onResultdoes — don't conflate the two;or_elsein Odin always takes a single replacement value/expression.
Rust trait bounds become Odin parametric ($T) procs constrained with
where clauses against core:intrinsics type queries, since Odin has no
trait system to express the bound directly.
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut max = items[0];
for &x in items {
if x > max { max = x; }
}
max
}import "base:intrinsics"
largest :: proc(items: []$T) -> T where intrinsics.type_is_ordered(T) {
max := items[0]
for x in items {
if x > max { max = x }
}
return max
}fn foo<T: Trait>(x: T)→foo :: proc(x: $T) where <intrinsics query that captures "implements Trait">. There usually isn't an exactintrinsicsquery for an arbitrary user trait — only for built-in properties (type_is_numeric,type_is_ordered,type_is_comparable,type_is_integer,type_is_float,type_has_field, etc.). If the Rust's bound is a project-defined trait with real, multiple implementations, see §Traits instead — that's dynamic-dispatch territory, not awhere-clause-expressible static bound.fn foo<T>(x: T) where T: Default→ if used only to get a zero value, just writex: T = {}inline — Odin's zero value already is the default for every type, so the bound usually disappears entirely.- Multiple type parameters (
fn foo<K, V>(...)) →foo :: proc(...) -> ... where ...with$K,$Vas separate parametric placeholders, same as Rust. - A generic
struct Foo<T> { x: T }→Foo :: struct($T: typeid) { x: T }— Odin's parametric structs take the type parameter as an explicittypeidparameter in the struct's own parameter list, instantiated asFoo(i32)at use sites (vs. Rust'sFoo<i32>). - Const generics (
struct Foo<const N: usize>) → Odin supports this directly:Foo :: struct($N: int) { data: [N]i32 }, instantiated asFoo(16). impl<T> Foo<T> { fn bar(&self) {} }→ a free proc taking the parametric struct:foo_bar :: proc(f: ^Foo($T)) { ... }.
This is the largest structural gap. Rust trait objects (dyn Trait,
Box<dyn Trait>) give you a single value type that dispatches to
different code depending on the concrete type it was built from, decided
at runtime. Odin has no trait-object type. Pick one of these, in order of
preference:
-
Closed set of implementors known at port time → a tagged union of the concrete types, dispatched with a type-
switch. This is almost always the right answer when the trait has a handful ofimpls in the same crate, and gives you the exhaustiveness check for free.trait Shape { fn area(&self) -> f64; } impl Shape for Circle { fn area(&self) -> f64 { ... } } impl Shape for Square { fn area(&self) -> f64 { ... } } fn total_area(shapes: &[Box<dyn Shape>]) -> f64 { shapes.iter().map(|s| s.area()).sum() }
Shape :: union { Circle, Square } shape_area :: proc(s: Shape) -> f64 { switch v in s { case Circle: return circle_area(v) case Square: return square_area(v) } return 0 } total_area :: proc(shapes: []Shape) -> f64 { total: f64 for s in shapes { total += shape_area(s) } return total }
-
Open set (plugin-style, or genuinely unbounded implementors, e.g. a trait crate consumers implement) → an explicit vtable struct of procedure pointers, paired with a
rawptr(orany) for the instance data — this is what the Rust compiler generates for you under adyn Trait, made manual:Shape_VTable :: struct { area: proc(self: rawptr) -> f64, } Shape :: struct { data: rawptr, vtable: ^Shape_VTable, } shape_area :: proc(s: Shape) -> f64 { return s.vtable.area(s.data) }
Leave
// TODO(port): hand-rolled vtable for dyn Traitat the construction sites so Phase B can sanity-check the function-pointer wiring. -
The trait is only ever used for a single concrete type at each call site (monomorphized statically, never actually boxed/erased at runtime) → it isn't really dynamic dispatch at all; treat it as a generic bound instead, per §Generics.
For a trait used purely for ergonomics in Rust — Display/Debug/
From/Into/operator-overload traits (Add, Index, etc.) — don't reach
for a vtable; Odin doesn't support operator overloading or method-call
sugar for user types at all (it does for built-in numeric arrays/matrices/
complex/quaternion types only), so these become plain named procs:
Add :: proc(a, b: Vec2) -> Vec2, called explicitly as Add(a, b), not
a + b. From/Into conversions become explicitly named converter procs
(vec2_from_array, to_array).
A Rust proc group is the closest thing Odin has to ad-hoc overloading-by-type, and is the right tool when a Rust trait's only real job was picking between a few concrete implementations by argument type:
// Rust: fn process(x: impl Into<Event>) — called with several concrete types
process :: proc{process_click, process_key, process_resize}Calls resolve to the matching overload at compile time by argument types — useful, but it is not runtime dispatch; don't reach for it where the Rust trait object's value was genuinely decided at runtime.
Odin has no Iterator trait and no combinator chain (.map().filter() .fold()). Rewrite combinator chains as explicit loops; don't hand-roll a
generic iterator abstraction unless the Rust file defined its own
(non-stdlib) iterator type that's reused across many call sites.
| Rust | Odin |
|---|---|
items.iter().map(f).collect::<Vec<_>>() |
result: [dynamic]T; for x in items { append(&result, f(x)) } |
items.iter().filter(pred).collect() |
result: [dynamic]T; for x in items { if pred(x) { append(&result, x) } } |
items.iter().fold(init, f) |
acc := init; for x in items { acc = f(acc, x) } |
items.iter().any(pred) / .all(pred) |
found := false; for x in items { if pred(x) { found = true; break } } (invert the condition and early-break for .all) |
items.iter().find(pred) |
for x in items { if pred(x) { /* found x */ } } with an ok flag, or use core:slice's linear-search helpers if available for the exact case |
|x| x + 1 (closure, no captures) |
proc(x: int) -> int { return x + 1 } — a plain proc value, assignable to a variable of proc(int)->int type |
|x| x + captured_var (closure capturing by value) |
Odin closures can capture, but allocate the closure's captured environment (via context.allocator) and are intended for short-lived use, not stored long-term the way Rust closures often are. For anything stored in a struct field or outliving the enclosing scope, prefer an explicit struct holding the captured data plus a plain (non-capturing) proc that takes that struct as a parameter — this matches what the Rust compiler does internally for a capturing closure anyway, made explicit. |
a hand-rolled struct MyIter { ... } + impl Iterator for MyIter |
a struct + an explicit my_iter_next :: proc(it: ^My_Iter) -> (T, bool), used in a for v, ok := my_iter_next(&it); ok; v, ok = my_iter_next(&it) { ... } loop, or wrap it in Odin's for ... in iterator-proc form if your Odin version supports custom for-in iterators for the type |
Odin has no async runtime — no async fn, no .await, no executor.
There is no drop-in equivalent; pick a real concurrency strategy at port
time rather than faking async:
async fn/.awaitchains driven bytokio/async-std→ restructure as either (a) a blocking call on a dedicated OS thread (core:thread.create), if the original concurrency was about not blocking a single-threaded reactor and you now have real threads available, or (b) an explicit state machine (a struct holding "what step are we on" plus whatever the.awaitpoints were waiting on), driven by your own poll loop, if you must preserve cooperative single-threaded scheduling. This is rarely mechanical — leave// TODO(port): was async, needs a real concurrency designand do not attempt to guess which of the two the surrounding system needs.std::thread::spawn→thread.create(proc_value)(core:thread),thread.join(t)to wait,thread.destroy(t)to free the handle.Mutex<T>/RwLock<T>→sync.Mutex/sync.RWMutex(core:sync), see the type-map row above — remember there's no lock-guard RAII, pair every lock with an explicitdeferunlock.AtomicUsize/AtomicBool/etc. (std::sync::atomic) →core:sync's atomic procs (e.g.sync.atomic_load,sync.atomic_add,sync.atomic_compare_exchange_strong) operating on a plain integer field — Odin doesn't wrap atomics in a distinctAtomic*field type the way Rust does; the atomicity is in how you access an ordinary field, so document which fields are accessed atomically with a comment, since the type signature alone won't tell a reader.- Channels (
std::sync::mpsc,crossbeam::channel) → no built-in channel type. Hand-roll a small ring buffer guarded by async.Mutex+sync.Cond, or — if the project already has one — reuse it rather than rewriting from scratch each file. Flag// TODO(port): was mpsc channel. Send/Syncmarker traits (compile-time thread-safety proof) → no equivalent exists; Odin will not stop you from sharing a non-thread-safe type across threads. Re-verify by hand anywhere the Rust'sSend/Syncbounds were load-bearing, and leave// TODO(port): verify thread-safety — was enforced by Send/Sync in Rustat the boundary.
Mirrors the spirit of Rust's &str/String split, with one extra wrinkle:
Odin's string carries no compiler-enforced UTF-8 guarantee.
&str(borrowed, read-only) →string.String(owned, growable) → build withstrings.Builder(import "core:strings"):b: strings.Builder; strings.write_string(&b, "..."); finalize withs := strings.to_string(b). Free the builder's backing buffer withstrings.builder_destroy(&b)once you're done with the resulting string (ordeferit right after declaringb, if the finalstringis copied out viastrings.clonebefore the builder dies).format!(...)→fmt.tprintf(...)for a temp-allocator string valid until the nextfree_all(context.temp_allocator)(fine for short-lived use like log lines and error messages consumed immediately), orfmt.aprintf(...)for a heap string the caller owns and mustdelete. Never store atprintfresult past the current frame/iteration.s.push_str("...")/s += "..."→strings.write_string(&builder, "...")while still building; once finalized to a plainstring, Odin strings are immutable views — there's no in-place append on a finishedstring, you go back to aBuilder.s.to_uppercase()/.to_lowercase()→strings.to_upper(s)/strings.to_lower(s)(core:strings) — these allocate and return a newstring, same as Rust.s.split(',')→strings.split(s, ",")returns[]string(caller frees withdelete), or iterate lazily withstrings.split_iteratorif avoiding the allocation matters.s.trim()→strings.trim_space(s).s.contains(needle)/.starts_with(p)/.ends_with(p)→strings.contains(s, needle)/strings.has_prefix(s, p)/strings.has_suffix(s, p).CString/*const c_char(FFI strings) →cstring, Odin's built-in null-terminated string type. Convert:strings.clone_to_cstring(s)(allocates, caller frees) to go fromstring→cstring;string(cstr)to go the other way (borrows, no allocation, scans for the NUL).
| Rust | Odin | notes |
|---|---|---|
Vec<T> |
[dynamic]T |
see Type map for the method table |
HashMap<K, V> |
map[K]V |
wyhash-family default hasher in both — iteration order is unspecified in both, don't rely on it in either |
HashSet<T> |
map[T]bool |
|
BTreeMap<K, V> |
hand-rolled sorted structure, see Type map | |
VecDeque<T> |
hand-rolled ring buffer, see Type map | |
BinaryHeap<T> |
core:container/priority_queue |
use the package, don't hand-roll |
[T; N] |
[N]T |
|
&[T] |
[]T |
|
Vec<Vec<T>> (2D) |
[dynamic][dynamic]T, or a flat [dynamic]T with manual row*width+col indexing if the Rust was already doing the latter for cache locality |
if the Rust used a flat buffer specifically for perf, keep doing that — don't "simplify" to nested dynamic arrays, that's a regression |
slice.sort() / .sort_by(cmp) |
slice.sort(s) / slice.sort_by(s, cmp) (core:slice) |
|
slice.binary_search(x) |
slice.binary_search(s, x) (core:slice) |
|
slice.iter().rev() |
for i := len(s) - 1; i >= 0; i -= 1 { x := s[i]; ... } |
no lazy reverse-iterator adapter; write the loop |
extern "C" {
fn sqrt(x: f64) -> f64;
}foreign import libm "system:m"
foreign libm {
sqrt :: proc "c" (x: f64) -> f64 ---
}extern "C" { fn foo(...); }blocks →foreign import <name> "<library>"followed by aforeign <name> { ... --- }block listing the signatures, terminated with---per declaration (no body).#[link(name = "m")]→ the string argument toforeign import("system:m"links the system library by name; a path string links a specific file).#[no_mangle] pub extern "C" fn foo(...)(exporting a Rust fn to C) →@(export) foo :: proc "c" (...) -> ... { ... }.repr(C)struct crossing the FFI boundary → a plain Odin struct, per the type-map row — no annotation needed, Odin's default layout already matches the C ABI.*const c_char/*mut c_charat the FFI boundary →cstringin the Odin signature (Odin treatscstringas exactly this C type), converted to/from Odinstringat the boundary per §Strings.- Calling convention: Rust's
extern "C"→ Odin'sproc "c" (...). If the Rust usedextern "system"(the Windows-specific convention), useproc "system" (...)in Odin to match. - If your file has externs and isn't already the project's dedicated FFI
package for this library, leave them in place with
// TODO(port): move to <area>_sys-equivalent package, mirroring whatever the project's convention is for isolating raw bindings.
#[cfg(target_os = "windows")]
fn foo() { /* windows impl */ }
#[cfg(not(target_os = "windows"))]
fn foo() { /* other impl */ }when ODIN_OS == .Windows {
foo :: proc() { /* windows impl */ }
} else {
foo :: proc() { /* other impl */ }
}#[cfg(target_os = "...")]→when ODIN_OS == .Windows/.Darwin/.Linux(etc. — full list incore:os/theOdin_OS_Typeenum).whenis a genuine compile-time branch like Rust's#[cfg]— the untaken branch isn't type-checked or compiled, so platform-only identifiers in the dead branch are fine, unlike a runtimeif.#[cfg(target_arch = "...")]→when ODIN_ARCH == .amd64/.arm64/ etc.#[cfg(unix)]→when ODIN_OS == .Linux || ODIN_OS == .Darwin || ODIN_OS == .FreeBSD(Odin has no single "is POSIX-ish" constant the waycfg(unix)is pre-bundled in Rust — enumerate the Unix-family OSes you actually need, or define your ownIS_POSIX :: ODIN_OS == .Linux || ...constant once and reuse it).#[cfg(debug_assertions)]→when ODIN_DEBUG(set via the-debugbuild flag), orODIN_DISABLE_ASSERTchecks if specifically gating assertion-only code.#[cfg(feature = "foo")](Cargo feature flags) → no built-in feature system; gate on a project-defined compile-time constant set via-define:FOO_ENABLED=trueon the command line, checked withwhen #config(FOO_ENABLED, false)(thecore:#configtwo-argument form supplies the default when the-defineis absent).- Runtime-checked platform branching where both branches could compile
(no platform-only API used in either) →
if ODIN_OS == .Windows { ... } else { ... }(plainif, notwhen) is fine and slightly cheaper to read when there's nothing platform-exclusive in either arm — but default towhenwhenever either branch references a Windows-only/Unix-only symbol, exactly mirroring why the original Rust needed#[cfg]rather than a runtimeif.
Odin has no macro system — no macro_rules!, no proc macros, no
derive. Every Rust macro use needs a real decision, not a mechanical
substitution:
- A
macro_rules!used purely for repetition avoidance (the same shape of code for several types) → a parametric proc ($T) if the bodies are truly identical modulo type, per §Generics. If the macro generates structurally different code per invocation (different field names, different counts of something), there's no equivalent — write the expansions out by hand and leave// TODO(port): was macro_rules!, hand-expanded — N call sites. - A
#[derive(Trait)]from an external crate (serde::Serialize,thiserror::Error, etc.) → there is no derive step; implement the equivalent behavior by hand per type (see §Error handling forthiserror, and write explicit (de)serialization procs forserde— Odin'score:encoding/jsonworks via reflection for the common case, so a hand-rolledMarshal-shaped proc is often unnecessary; check whether the default reflection-based (de)serialization already produces the same wire format before writing one). - A proc macro that generates a whole
implblock or new items (#[tokio::main], a code-generating attribute macro) → there is no Odin equivalent at all. Read what the macro expands to (check the crate's docs orcargo expandoutput) and hand-write the expansion. Always leave// TODO(port): was proc-macro <name>, hand-expanded. - Declarative macros used as lightweight DSLs (e.g.
vec![1, 2, 3],matches!(x, Pattern)) → these have direct non-macro equivalents:vec![1, 2, 3]→ a dynamic array literal[dynamic]int{1, 2, 3}(ormake+appends);matches!(x, Pattern)→ an inlineswitch/ifexpression, written out.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}package foo
import "core:testing"
@(test)
test_it_works :: proc(t: ^testing.T) {
testing.expect_value(t, 2 + 2, 4)
}#[test] fn name()→@(test) test_name :: proc(t: ^testing.T) { ... }— note the proc takes a^testing.Tparameter, unlike Rust's zero-argument test functions.assert_eq!(a, b)→testing.expect_value(t, a, b).assert!(cond)inside a test →testing.expect(t, cond)(ortesting.expectf(t, cond, "message %v", ...)for a formatted failure message).- A Rust
mod tests { ... }submodule → Odin has no inline module blocks; the@(test)-attributed procs simply live in the same file (or package) as the code under test — there's no need to wrap them in a separate construct, and no#[cfg(test)]gate is needed since@(test)procs are already excluded from non-test builds automatically. - Run with
odin test .(orodin test <path>) from the package directory.
useimport lines at the top of the file → becomeimportstatements at the top of the.odinfile, but don't try to preserve a 1:1 mapping of every individual imported item — Odin imports a whole package and you reference items aspackage.Item; consolidate.mod foo;lines that exist purely to declare a submodule file → nothing to write; the directory/file structure itself is the module declaration in Odin (nomodstatement needed).#[allow(...)]/#[warn(...)]lint-suppression attributes → drop; Odin's compiler warnings/lints are not configured per-item this way.- Build scripts (
build.rs) andCargo.toml→ no equivalent file to port; any codegenbuild.rswas doing needs a real decision (a standalone Odin/shell generator script, or hand-written output), flagged// TODO(port): had a build.rs codegen step. #[derive(serde::Serialize, serde::Deserialize)]on a pure data struct with no custom#[serde(...)]attributes → likely nothing to write, see §Macros — check whethercore:encoding/json's reflection-based default already matches before hand-writing anything.- Doc comments (
///,//!) → carry over as plain//comments directly above the item; Odin has no separate doc-comment syntax or doc-generator convention to preserve beyond that.
End your .odin with a trailer comment:
// ──────────────────────────────────────────────────────────────────────────
// PORT STATUS
// source: <path/to/file>.rs (NNN lines)
// confidence: high | medium | low
// todos: N
// notes: <one line: anything Phase B needs to know>
// ──────────────────────────────────────────────────────────────────────────confidence: low means "logic is probably wrong, re-read the Rust in Phase
B." medium means "imports/package wiring will need fixing but logic is
right." high means "should compile with only mechanical import fixes" —
reserve high for files with no trait objects, no async, and no macros to
resolve, since those three categories are where this port is least
mechanical.