The Data-Oriented Language for Sane Software Development.
Odin Language fork focused on exploring language design, memory safety, and other completely subjective things
-
One of the goals of this fork is to make Odin a safer language, breaking the implicit patterns from the C language, improving code readability, and making it more enjoyable to work with memory.
-
I love Odin, but I don't like how it hides allocations from the user and tries to handle lots of behavior implicitly. If an allocator is invalid, there should be no fallback. A buggy code is a buggy code and should crash. Typing one extra word is an absolute worth trade-off over losing track of how your memory is managed. We shouldn't hide when memory is mentioned.
-
Besides that, I also changed a lot of things I consider subjective. I'm taking this as an experiment around language design, as I think it's fun.
-
Feel free to use at your own discretion. That might be a LOT of things you don't agree with. I went far enough to remove libraries I won't use, so this is not really for public use, but I decided to keep this as a public fork so I could share a little bit of some of my design choices.
-
Currently, all changes are in
.odinfiles. No changes were made to the.cppsource code, so the compiler is untouched. Maybe I'll change the compiler if this would result in improvements that could not be achieved by the Odin language alone.
- This is the new signature for
context:
Context :: struct {
user_ptr: rawptr,
user_index: int,
_internal: rawptr,
}- Also,
runtime.default_context()was removed. If you want a blank context, just usecontext = {}. - Context is only used for interop with third-party APIs.
- Every other entry was removed and replaced with something less invasive.
- You should only care about
contextif you are aiming to interact with an external API, as follows:
import "core:fmt"
Third_Party_API :: struct {
callback_with_limiting_signature: #type proc(),
}
your_callback_implementation :: proc() {
my_array := cast(^[]int)context.user_ptr
fmt.println(my_array)
// Prints: &[4, 1, 3, 2]
}
main :: proc() {
api := Third_Party_API{
callback_with_limiting_signature = your_callback_implementation
}
my_array := []int{ 4, 1, 3, 2 }
context.user_ptr = &my_array
api.callback_with_limiting_signature()
}
- If this is not the case,
contextwill serve you no purpose and can be ignored. - The default
"odin"calling convention should still be used for consistency and for better future third-party integrations with your codebase.
context.allocatorwas removed.
main :: proc() {
a := make([dynamic]int)
// context.allocator was used implicitly.
defer delete(a)
b := new(int)
// context.allocator was used implicitly.
defer free(b)
// context.allocator was used implicitly.
}- All uses of allocators is enforced to be explicit.
- No code inside the library uses implicit allocators.
- The signature
allocator :=was replaced byallocator: mem.Allocator.
import "base:runtime"
main :: proc() {
allocator := runtime.heap_allocator()
a := make([dynamic]int, allocator)
// `make` *requires* an explicit allocator. If not complied, there will be a compile-time error.
defer delete(a)
// No need to be explicit about the allocator here, as a `[dynamic]` array stores the allocator.
b := new(int, allocator)
// `new` *requires* an explicit allocator. If not complied, there will be a compile-time error.
defer free(b, allocator)
// `free` *requires* an explicit allocator. If not complied, there will be a compile-time error.
}- In this example, the
runtime.heap_allocator()was used, which is the same allocator from the previouscontext.allocator. You'll have the same allocation behavior, but now the allocation has to be explicit and is no longer tied to thecontextsystem. More on that later.
- Before
main :: proc() {
a: [dynamic]int
append(&a, 1)
// context.allocator was used implicitly to allocate `a`.
}-
This behavior is no longer allowed, as it only leads to confusing and buggy code. This was especially a problem when using custom allocators, but forgetting to initialize an array/map, making the data be allocated without the usage of your custom allocator.
-
This now results in a runtime assertion.
-
After
main :: proc() {
a: [dynamic]int
append(&a, 1)
// Runtime assertion, indicating that no allocator was used for `a` and the array should be initialized.
}- This is not allowed in Odin:
aprint :: proc(args: ..any, sep := " ", allocator: mem.Allocator) -> string { }aprinthas a variadic argument (args), followed by an argument without a default value.- To make this work, this change was made:
aprint :: proc(args: []any, sep := " ", allocator: mem.Allocator) -> string { }- The procedure no longer supports the variadic argument.
- I also thought about these signatures:
- Passing the
allocatorfirst would allow the variadic argument to exist, but this would break the convention of passing the allocator as the last parameter.
aprint :: proc(allocator: mem.Allocator, args: ..any, sep := " ") -> string { }
- Using a default value for
allocatorthat would result in a panic. This is objectively a worse solution than the one I went with, as this only generates an error at runtime, while not using a default value generates an error at compile time.
aprint :: proc(args: ..any, sep := " ", allocator: runtime.COMPTIME_PANIC_ALLOCATOR) -> string { }
- Passing the
- The solution I went with is not as ergonomic as the one from Odin, but this is a fair price to avoid an implicit allocation.
aprint :: proc(args: ..any, sep := " ", allocator := context.allocator) -> string { }
msg := aprint(1, 2, 3, 4)
// This used the context.allocator implicitly.aprint :: proc(args: []any, sep := " ", allocator: mem.Allocator) -> string { }
msg := aprint({1, 2, 3, 4}, allocator = my_allocator)
// The procedure now *requires* an explicit allocator. If not complied, there will be a compile-time error._startup_runtimeand_cleanup_runtimewere removed.- This means that
@initand@finino longer work and have to be manually called. - Patterns like
a: T = b()no longer work as well; I've only found this pattern inside thecore:os/os2/process.odinforargs := get_args()in the global scope. - I wish using these annotations or calling a function in the global scope would be a compiler error, but for now this is not the case, and you have to ensure this doesn't happen. If it does, the operation will be ignored by the compiler.
- Check this Proposal to know more.
- Check Odin#Entry Point to know more.
- Worth noticing that after the removal of
@initand@fini, there are a lot fewer procedures in the engine that require the"contextless"signature, as a lot of them were made this way just as a way to ensure compatibility between the@init/@finiprocedures, since they were required to be"contextless".
- The
core:oslibrary was removed, replaced bycore:os/os2. - There are plans for Odin to replace the libraries in 2026, but as I was changing the codebase so much for safety reasons, I decided to rush the replacement, as the old
core:oshad a lot of implicit behavior everywhere, andos2is a far better library. - If you see
import "core:os", this refers to the newos2, simply renamed toos.
- The
core:os/os2no longer allocates anything implicitly inside its library. All allocators defined in this library were removed, and now every allocation uses a user-defined allocator; you can pass in aruntime.heap_allocatorif you want the exact previous behavior.
runtime.default_temp_allocator()was removed, replaced byruntime.temp_allocator.- The
context.temp_allocatoralready was a thread-local variable in Odin, but now this behavior is clear. - The
runtime.temp_allocatorhas to be initialized manually. - There was an overhaul in the default allocators section inside the runtime.
- As for the removal of
@initand@fini, as well as an overhaul of temporary allocations, I decided to remove the responsibility of thecore:threadlibrary to manage temporary allocations. - The user should be responsible for managing the
runtime.temp_allocator's lifetime. This is done exactly the same way as for the main thread:
runtime.temp_allocator_init(0, backing_temp_allocator)
defer runtime.temp_allocator_destroy()- The
backing_temp_allocatorhere could be any allocator. If you want the same behavior as the previouscontext.temp_allocator, just useruntime.heap_allocator()as thebacking_temp_allocator.
- After all the changes, there are NO implicit allocations with
runtime.heap_allocator/context.allocator(removed) /os2.heap_allocator(removed) /os2.file_allocator(removed) anywhere in the Odin libraries. - The
runtime.temp_allocator(previouscontext.temp_allocator) still does implicit allocations; after testing with this, I decided to keep it as implicit, as it was a lot of trouble having to pass this allocator in every one of its VAST uses. - This is the ONLY code inside all Odin libraries that refers to implicit allocations, located on
base:runtime/default_temp_allocator.odin.
// Temp Allocator
when ODIN_ARCH == .i386 && ODIN_OS == .Windows {
// Thread-local storage is problematic on Windows i386
temp_allocator: Allocator
@(private="file") temp_allocator_arena: Arena
} else {
@(thread_local) temp_allocator: Allocator
@(thread_local, private="file") temp_allocator_arena: Arena
}
temp_allocator_init :: proc(size: uint, backing_temp_allocator: Allocator) {
// Temp Allocator Arena, using the Backing Temp Allocator
err := arena_init(&temp_allocator_arena, 0, backing_temp_allocator)
assert(err == nil, "Failure initializing the arena")
// Temp Allocator, using the Temp Allocator Arena
temp_allocator = arena_allocator(&temp_allocator_arena)
}
temp_allocator_destroy :: proc() {
arena_destroy(&temp_allocator_arena)
}
@(deferred_out=arena_temp_end)
TEMP_ALLOCATOR_TEMP_GUARD :: #force_inline proc(collision: Allocator = {}, loc := #caller_location) -> (Arena_Temp, Source_Code_Location) {
if collision == temp_allocator {
return {}, loc
}
return arena_temp_begin(&temp_allocator_arena, loc), loc
}context.assertion_failure_procwas removed, replaced byruntime.assertion_failure_proc.runtime.assertion_failure_procwas created to now hold the user customization of how an assertion should be.- For clarity,
assert,panic,ensureandunimplementedare now all"contextless"by default. The old counterparts were removed (assert_contextless, etc). - Flaws from the old implementation with
context.assertion_failure_proc:
println_any :: #force_no_inline proc "contextless" (args: ..any) {
context = default_context()
loop: for arg, i in args {
assert(arg.id != nil)
// ^^ This assertion will *not* be the one defined by the user, but the default one from the `context = default_context()`
if i != 0 {
print_string(" ")
}
print_any_single(arg)
}
print_string("\n")
}- Having the 'user customization' decoupled from the
contextensures uniformity for any calling convention used. - Now assertions from within
"contextless"procedure will respect the user-defined behavior, defined in theruntime.assertion_failure_proc. - New implementation:
Assertion_Failure_Proc :: #type proc "contextless" (prefix, message: string, loc: Source_Code_Location) -> !
assertion_failure_proc: Assertion_Failure_Proc = default_assertion_failure_proc
@(disabled=ODIN_DISABLE_ASSERT)
assert :: proc "contextless" (condition: bool, message := #caller_expression(condition), loc := #caller_location) {
if !condition {
@(cold)
internal :: proc "contextless" (message: string, loc: Source_Code_Location) {
assertion_failure_proc("runtime assertion", message, loc)
}
internal(message, loc)
}
}- The
runtime.assertion_failure_proccan be changed by the user. - The default for
context.assertion_failure_procandruntime.assertion_failure_procis the same. If you haven't changed the oldcontext.assertion_failure_procto anything different, the behavior will be the same.
context.loggerwas removed, replaced bylog.default_logger.- The logger provides a system where the API provides the entry points so that user-defined logging implementations can print the messages in the terminal.
- The idea is ok, but I don't think it's that practical.
- The API can choose to override the
context.loggerand use whatever it sees fit. It is not enforced that a library shouldn't touch the user-defined configurations. - Not every api uses log. This is not enforced. A lot of APIs just print with
fmt. - A lot of APIs don't actually print anything.
- There's no room for easy coloring of the things in the terminal. There's a fixed template following the predefined
Optionsin the library. One could wrap the log procedures or create their own logging procedure, but trying to print something colorful and customizable is not ergonomic at all with thelogAPI. - I have never seen a third-party Odin API that uses the logger to its full extent.
- In the end, the library just seems limiting and extremely situational.
- It doesn't seem practical, even tho I can appreciate the idea.
- If the point of
contextis to improve interoperability with bad APIs, usingcontext.loggerconsistently and correctly seems like the last thing they would do. - To be fair, the ONLY reasons I have ever used logger were: prints
#caller_location, and makes error red, warns yellow. It was never for logging flexibility between the user and the API. There is no clear intuition for this. - The library itself is ok, but I don't think something so situational, misused, or impractical should be part of the runtime. Just like
core:fmt, this should only becore. - The
contextusage for it seems really hard to justify. - Is there a way the logger could be built that would make it reliable and a standard for any API that wants to use it? I'm not sure, I just don't think the current implementation solves that.
- My changes don't remove
core:log, just make logger decoupled from theruntimeandcontext.
context.random_generatorwas removed, replaced byruntime.global_random_generator.- Same argument as previously removed patterns from
context. - A
Random_Generatoris no longer used implicitly in any library.