Odin Changes in Detail #
New and Improved core:os #
Odin has been designed to be a pragmatic and evolutionary language, and as such, most people have come to appreciate the results of that, especially stability of language features. Odin rarely experiences breaking changes, however we have some technical debt to pay. In the previous article Moving Towards a New “core:os” from October 2025, we discussed the rationale behind updating package core:os and the general design improvements that come with it.
If you require the old functionality of core:os, it is still available under core:os/old, but this will be removed in the future (Q3 2026).
#+feature using-stmt #
using as a statement has been a controversial feature of Odin for a long time. using on struct fields has been shown to be an extremely useful construct with very few issues in practice but the use of using as a statement has caused problems in general. We are not removing the feature but instead making using as a statement and procedure parameter modifier an opt-in feature on a per-file basis rather than having it on by default. This can be enabled by placing #+feature using-stmt to the top of the file that requires it.
using on struct fields still works as expected and will continue to do so.
Link Time Optimization (LTO) Support #
Link Time Optimization (LTO) is a technique which allows the compiler to improve the performance of a program by optimizing it at the linking stage, allowing the compiler to analyse and optimize across multiple translation units. This can lead to better runtime performance by removing unused code and better support for inlining procedures from different translation units.
This can be abled in Odin with the compiler flags: -lto:thin and -lto:thin-files, and the flags will enable -use-separate-modules (if not already set) and default to -linker:lld.
-lto:thin(one module per package)-lto:thin-files(one module per file)
Tail Call Support with #must_tail #
Odin now supports the explicit ability to state how to opimize for tail calls through the new directive #must_tail and new calling conventions "preserve/none", "preserve/most", and "preserve/all".
- Note: The use of tail calls may cause issues with address sanitization tooling due to its nature.
- Note: This is a very advanced feature, and should only be used by people who know how to use it and need it.
Code example:
Op_Code :: enum i32 {
PUSH, ADD, SUB, MUL, DIV, HLT,
}
Instr :: struct {
opc: Op_Code,
imm: i32,
}
VM :: struct {
stack: [10]i32,
sp: int,
}
push :: proc "contextless" (vm: ^VM, v: i32) {
vm.stack[vm.sp] = v
vm.sp += 1
}
pop :: proc "preserve/none" (vm: ^VM) -> i32 {
vm.sp -= 1
return vm.stack[vm.sp]
}
// The tail-calling threaded interpreter approach
exec :: proc "preserve/none" (vm: ^VM, instrs: [^]Instr) -> i32 {
do_push :: proc "preserve/none" (vm: ^VM, instrs: [^]Instr) -> i32 {
push(vm, instrs[0].imm)
return #must_tail exec(vm, instr[1:])
}
do_add :: proc "preserve/none" (vm: ^VM, instrs: [^]Instr) -> i32 {
push(vm, pop(vm) + pop(vm))
return #must_tail exec(vm, instr[1:])
}
do_sub :: proc "preserve/none" (vm: ^VM, instrs: [^]Instr) -> i32 {
push(vm, pop(vm) - pop(vm))
return #must_tail exec(vm, instr[1:])
}
do_mul :: proc "preserve/none" (vm: ^VM, instrs: [^]Instr) -> i32 {
push(vm, pop(vm) * pop(vm))
return #must_tail exec(vm, instr[1:])
}
do_div :: proc "preserve/none" (vm: ^VM, instrs: [^]Instr) -> i32 {
push(vm, pop(vm) / pop(vm))
return #must_tail exec(vm, instr[1:])
}
do_hlt :: proc "preserve/none" (vm: ^VM, instrs: [^]Instr) -> i32 {
return pop(vm)
}
@(static, rodata) LUT := [OP_Code](proc "preserve/none" (^VM, [^]Instr) -> i32) {
.PUSH = do_push,
.ADD = do_ADD,
.SUB = do_SUB,
.MUL = do_MUL,
.DIV = do_DIV,
.HLT = do_HLT,
}
return #must_tail LUT[instrs[0].opc](vm, instrs)
}
Licensing Changes to zlib from BSD 3-clause #
Odin has recently changed its licence for the source code from BSD 3-clause to zlib. This decision was driven by a design for greater simplicity, legal clarity, and to remove many potentially restrictive clauses.
Both the Odin compiler source code and the bundled library collections (base and core) will be under the new zlib licence. Each vendor package might have its own licence.
Native Support for UTF-16 strings: string16 and cstring16 #
Odin by default supports UTF-8 and Unicode out-of-the-box.
chacha8rand as the Default Random Generator #
Odin’s default context.random_generator is now based on the chacha8rand CSPRNG generator seeded from system entropy by default.
Note: This is a breaking change since the output for a given seed will be different from the previous PCG64 generator.
We have heavily optimized this generator with SIMD to ensure it is both fast and have good properties.
struct #all_or_none and struct #simple #
struct #all_or_none is a new struct directive which requires that struct literals must have all or none of their fields set when declaring a compount literal with named fields.
struct #simple is a new struct directive which forces a struct to use simple comparison if all of the fields “nearly simply comparable”. This is a niche solution to a niche problem. “Simply comparable” are types which can be compared with the equivalent of C’s memcmp directly (e.g. integers, booleans, aggregates of them), and “nearly simply comparable” include the simply comparable types and floats, since floats have different rules for +0, -0, and NaN, and are NEARLY simply comparable if you don’t care about those edge cases. This struct directive will force a struct to be “simply comparable” even if its fields would make it “nearly simply comparable”.
New Packages #
core:nbio #
Documentation: https://pkg.odin-lang.org/core/nbio/
Package core:nbio implements a non-block I/O and event loop abstraction layer over several platform-specific asynchronous I/O APIs.
Examples: https://github.com/odin-lang/examples/tree/master/nbio
All main targets of Odin are supported:
- Windows (IOCP)
- Linux (io uring)
- Darwin, OpenBSD, NetBSD, FreeBSD (kQueue)
Targets that are not currently supported are stubbed out and compile, but will error on unimplementedness.
In the future, this package will be the basis of the core:net/http package, which is coming soon.
We already know we’ll get this question: Why callbacks? What about callback hell?!
Callbacks are the simplest interface an event loop can reasonably expose: run this when the operation completes. The loop itself does not need to know how the result is consumed1.
It is possible to apply other mechanisms that can be built on top of the callback system such as a coroutine system (either through something like Lua or a native coroutine package), or have a queue you continue at your own time (No Callback example).
Callbacks also allow multiple independent users to share the same event loop. A package can register its own operations, and the application code can register others, all without either seeing or handling the other’s completions.
core:container/xar #
Documentation: https://pkg.odin-lang.org/core/container/xar/
Package core:container/xar implements data structures related to exponential arrays. They are dynamically growing arrays using exponentially-sized chunks, providing stable memory addresses for all elements. Unlike [dynamic]T, elements never move once allocated, making it self to hold pointers to elements.
For more information about this data structure in general: https://azmr.uk/dyn/#exponential-arrayxar
core:container/handle_map #
Documentation: https://pkg.odin-lang.org/core/container/handle_map/
Package core:container/xar implements a generation-index based handle containers, both static and dynamic.
Example:
import hm "core:container/handle_map"
Handle :: hm.Handle32
Entity :: struct {
handle: Handle,
pos: [2]f32,
}
{ // static map
entities: hm.Static_Handle_Map(1024, Entity, Handle)
h1 := hm.add(&entities, Entity{pos = {1, 4}})
h2 := hm.add(&entities, Entity{pos = {9, 16}})
if e, ok := hm.get(&entities, h2); ok {
e.pos.x += 32
}
hm.remove(&entities, h1)
h3 := hm.add(&entities, Entity{pos = {6, 7}})
it := hm.iterator_make(&entities)
for e, h in hm.iterate(&it) {
e.pos += {1, 2}
}
}
{ // dynamic map
entities: hm.Dynamic_Handle_Map(Entity, Handle)
hm.dynamic_init(&entities, context.allocator)
defer hm.dynamic_destroy(&entities)
h1 := hm.add(&entities, Entity{pos = {1, 4}})
h2 := hm.add(&entities, Entity{pos = {9, 16}})
if e, ok := hm.get(&entities, h2); ok {
e.pos.x += 32
}
hm.remove(&entities, h1)
h3 := hm.add(&entities, Entity{pos = {6, 7}})
it := hm.iterator_make(&entities)
for e, h in hm.iterate(&it) {
e.pos += {1, 2}
}
}
core:crypto/ecdh #
Package core:crypto/ecdh is a unification of X25519 and X448, and adds support for secp256r1 and secp384r1.
For more information, see the PR: https://github.com/odin-lang/Odin/pull/6213
vendor:curl #
Documentation: https://pkg.odin-lang.org/vendor/curl/
Odin now bundles with libCURL as part of its vendor package; the free and open-source client-side URL transfer library that supports numerous internet protocols, including HTTP, FTP, and so much more.
For instance you could give control of the entire loop to the user, but that comes with all the problems with that. ↩︎