Types

This is a table that hold a comparison of all fundamental types you should know about compared to what they are in other languages:

Type:OdinC#
16 bit floatingf16
32 bit floatingf32float
64 bit floatingf64double
128 bit floatingdecimal
8 bit integer signedi8sbyte
8 bit integer unsignedu8byte
16 bit integer signedi16short
16 bit integer unsignedu16ushort
32 bit integer signedi32int
32 bit integer unsignedu32uint
64 bit integer signedi64long
64 bit integer unsignedu64ulong
128 bit integer signeds128
128 bit integer unsignedu128
pointer sized integeruintptrnint
booleanboolbool
characterrunechar
stringstring/cstringstring
no typeanyobject
type identifiertypeidType
nullnilnull

This table doesn’t show all of odin’s types, odin has many build in types compared to other languages, but all the important types are in this type table. Some examples of types that arent in the table are: b8, b16, b32, b64, string16, cstring16, rawptr, complex32, complex64, complex128, quaternion64, quaternion128, quaternion256, matrix, map, array. But to be fair, you do not need to know what these types all are, i just mentioned them so that you know they exist.

In odin types are first class, meaning types are values. This means procedures (functions) are values and can be assigned like any other value. All types like int and float are of type typeid.

a :: int // a is int
b: a = 1 // b is now an int

In Odin typeid is a bit of an overloaded term, because there is a difference between a compile time typeid and a runtime typeid. if it’s at compile time, typeid is a type, if it’s at runtime, typeid is an integer representing a type

x :: int // this works, x is a *type*
x := int // error, you'll need to do typeid_of(int)

variables can be declared in one of the following ways, the declaration syntax is similar to jai and golang in use

x: int // declaration
x: int = 4 // explicit type
x := 4 // inferred type
x :: 4 // inferred type constant
x :int: 4 // explicit type constant
x, y, z: int // multi variable declaration

Procedures (Functions)

In odin a function is called a procedure. Procedures are first class types, they can be assigned to variables like any other type.

A procedure literal in Odin is defined with the proc keyword, following this proc(input) -> (output) { ... } as syntax:

// basic procedure example
multiply :: proc(x: int, y: int) -> int {
    return x * y
}

List of all the ways to give inputs and outputs to a procedure:

// inputs (arguments)

proc() // no arguments
proc(x: int) // one argument
proc(x: int, y: int) // two arguments
proc(x, y: int) // equivelant to above
proc(x := 10) // default value for argument
proc(nums: ..int) // variable argument amount

// outputs (return values)

proc() // no return values
proc() -> int // one return value
proc() -> (int, int) // two return values
proc() -> (a: int, b: int) // two named return values
proc() -> (a, b: int) // equivelant to above
proc() -> (a := 10) // default value for return value
proc() -> (..int) // error (odin doesnt support variadic returns)
proc() -> (nums: ..int) // error (odin doesnt support variadic returns)

usage of default argument values:

foo :: proc(x := 10) { ... }
a = foo() // uses default value
b = foo(4) // overrides default value
b = foo(x=4) // overrides default value by name

usage of varying number of arguments (variadic parameters):

variadic parameters are just syntactic sugar for slices ..int and []int are identical in structure

sum :: proc(nums: ..int) -> int {
    for n in nums do result += n
    return result
}

sum() // 0
sum(1, 2) // 3
sum(1, 2, 3, 4, 5) // 15
sum(..slice) // passing by slice

usage of named outputs:

// without named output
foo :: proc() -> int {
    a := 10
    return a
}

// with named output and naked return statement
foo :: proc() -> (a: int) {
    a = 10
    return // return still required (returns the named return value)
}

Loops

Simple for loops:

Doing loops in Odin is very simple and c like, with some nice syntax choices, like merging for and while loops into one concept.

Types of loop syntaxes in odin language:

  • range based loop: for index in range for ranges.
  • collection loop: for value in collection for collections like arrays.
  • traditional loop: for init; condition; increment for everything else.

Various different loop examples:

// basic loop
for i := 0; i < 10; i += 1 {
    fmt.println(i)
}

// while loop equivelant
for i < 10 {
    fmt.println(i)
}

// range loop
for i in 0..<10 {
    fmt.println(i)
}

// array loop (array/slice/dynamic/string/map)
for value in some_array {
    fmt.println(value)
}

// named index array loop
for value, index in some_array {
    fmt.println(index, value)
}

// named key and value map loop
for key, value in some_map {
    fmt.println(key, value)
}

// infinite loop
for {
    fmt.println(i)
}

// nested loop
for i := 0; i < 10; i += 1 {
    for j := 0; j < 10; j += 1 {
        fmt.println(i, j)
    }
}

// nested array loop
for outer in outer_array {
    for inner in inner_array {
        fmt.println(inner, outer)
    }
}

// reverse loop
#reverse for x in array {
    fmt.println(x)
}

// array loop by reference
for &value in some_array {
    value = something // element can be modified
}

// map loop by reference (key can not be referenced)
for key, &value in some_map {
    value += 1
}

// single line loops with scope block
for i := 0; i < 10; i += 1 { }

// single line loop (using the do keyword)
for i := 0; i < 10; i += 1 do foo()

Ternary Operator

In Odin there are 3 types of ternary operator syntaxes:

foo = condition ? x : y // runtime ternary
foo = x if condition else y // runtime ternary
foo = x when condition else y // compile time ternary

References

In odin there are no references like in C++ and no reference types like in C#, Odin is similar to C in that you need a pointer to reference variables.

When passing a value as an argument, it passes as a pointer automatically if its more effecient this is enabled by the fact that all parameters are immutable in Odin, like a const& in cpp, making it feel like everything is always copied like in c.

Passing a pointer value always makes a copy of the pointer, not the data it points to.

slices, dynamic arrays, and maps have no special considerations here, they are normal structs with pointer fields, and are passed as such, in that regard they work like reference types.

Memory

Odin is a manual memory managed language, it requires you to allocate and free heap memory yourself. Stack memory is tied to its scope and freed automatically.

new() -> allocates a value of the type given and returns a pointer:

ptr: ^int = new(int)

free() - frees the memory at the pointer given:

ptr: ^int = new(int)
free(ptr)

make() - allocates memory for the backing data of either a slice, dynamic array, or map:

my_slice := make([]int, 100)
my_dynamic_array := make([dynamic]int, 100)
my_map := make(map[string]int, 100)

delete() - deletes the backing memory of a anything allocated with make:

delete(my_slice)
delete(my_dynamic_array)
delete(my_map)

Pointers

pointers have the same semantics as in c, but not the same syntax. using ^type as pointer types, ptr^ as dereference syntax, and &value as the address-of operator.

// c
int x = 1;
int* p = &x;
*p = 2;
// odin
x := 1
p: ^int = &x
p^ = 2

There is no such thing as pointer arithmetic like in c, because unlike in c, arrays are not just fancy pointers, but actual value types, for pointer arithmetic like behaviour there are “multi pointers” of the [^]T type, which are pointers that map to multiple items, and can be indexed like an array. multi pointers are easiest to use with the raw_data() builtin call. the raw_data is a builtin which returns the underlying data of a builtin data type as a multi pointer. the builtin make() procedure can also return multi pointers.

simple usage example of a multi pointer:

ptr: [^]int
arr := [3]int{10, 20, 30}
ptr = raw_data(arr[:]) // get multi pointer to a slice of the array
fmt.println(ptr, ptr[1], arr) // 0x7FFCBE9FE688 20 [10, 20, 30]

basic rules about what returns when indexing or slicing multi pointers:

mptr: [^]T
mptr[i] -> T // indexing
mptr[:] -> [^]T // slicing with full range
mptr[i:] -> [^]T // slicing with specific start
mptr[:n] -> []T // slicing with specific end
mptr[i:n] -> []T // slicing with specific start and end

what multi pointers support:

  • indexing
  • slicing (if both high and low operands are given)
  • implicit conversions between ^T and [^]T

What multi pointers do not support:

  • dereferencing

Modules And Imports

Instead of headers of modules or namespaces, the Odin language uses packages. Packages in odin are directory based, similar to how golang manages packages, this makes using submodules useful, and makes it so you dont need a package manager. In Odin a package is a directory of Odin code files, all of which have the same package declaration at the top. Make a file part of a package by putting the package package_name declaration at the top of the odin files in the package. A directory cannot contain more than 1 package, so you can not have different package declarations in the same directory. To import a package (make it accesable), you use the import keyword. To import a standard library package you can use a prefix like import "core:fmt" where core: is the collection prefix. If no prefix is specified the package will be searched relative to the current file path. Packages can be namespaced by using the import foo "core:fmt" syntax.

Standard Library

Initialization

Structs

Casting

In odin there is no implicit widening type conversion like in c, and only a very short list of implicit conversions nearly all types must be manually cast, luckily odin has a nice and simple syntax for type conversions and casting:

The expression T(value) converts value to the T type:

i: i64 = 123
f: f64 = f64(i)

The cast() operator can also be used to do the same thing:

i: i64 = 123
f: f64 = cast(f64)i

The transmute() operator is a bit cast conversion between two types of the same size, the transmute(T)value expression is the same as the (^T)(&value)^ expression:

i: i64 = 123
f: f64 = transmute(f64)i

Prodecure (Function) Overloading

Unlike in many other languages, procedure overloading in Odin is explicit.

Here is an example of how to do procedure overloading:

bool_to_string :: proc(b: bool) -> string {
    // convert bool to string
}

int_to_string :: proc(i: int) -> string {
    // convert int to string
}

// convert bool or int to string
to_string :: proc{bool_to_string, int_to_string}

Static Arrays / Slices / Dynamic Arrays

In odin a static array is typed like [N]T, or [?]T for inferred size (only in a literal), and a slice like []T, and a dynamic array like [dynamic]T, or [dynamic;N]T for a dynamic array with a fixed capacity.

An array in odin is just like a struct in that its value type that contains all of its own data.

A slice is just a view into an existing array, a slice is basically a pointer and length of an array. Any array can be turned into a slice using the array[low:high] syntax.

Dynamic arrays are similar to slices, but their lengths may change during runtime. Dynamic arrays are resizeable and they are allocated when needed using the current context allocator.

Slices and dynamic arrays are simple small structures with a pointer to an underlying array, therefore copying or passing a slice or dynamic array will not copy the underlying data. Because of this they work kinda like reference types. Fixed capacity dynamic arrays do however copy the data

The zero value of a slice is nil. A nil slice has a length of 0 and does not point to any underlying memory. Slices can be compared against nil and nothing else.

types of arrays:

Type:SyntaxValue Semantics
static array[N]Tholds data
slice[]Tpoints to data
dynamic array[dynamic]Tpoints to data
dynamic array (fixed capacity)[dynamic;N]Tholds data

static arrays:

// basic usage
array: [4]int // declare
array: [4]int = [4]int{1, 2, 3, 4} // initialize with literal
value: int = array[0] // index

// other ways of initializing
array: [4]int = [4]int{1, 2, 3, 4} // explicit literal type
array: [4]int = {1, 2, 3, 4} // inferred literal type
array := [4]int{1, 2, 3, 4} // inferred type
array := [?]int{1, 2, 3, 4} // inferred literal size

slices (array views):

// basic usage
slice: []int // declare
slice: []int = []int{1, 2, 3, 4} // initialize with literal
slice: []int = array[:] // get slice from array
value: int = slice[0] // index

// the following slicing operations are all equivelant (if array has a lenghth of 6)
slice := array[0:6]
slice := array[:6]
slice := array[0:]
slice := array[:]

dynamic arrays:

// basic usage
dyn_array: [dynamic]int // declare
dyn_array: [dynamic; 16]int // declare with max capacity
dyn_array := make([dynamic]int, 0, 4) // initialize with make
value: int = dyn_array[0] // index

// dynamic literals are not allowed because they hide allocations
// but they can be enabled by using #+feature dynamic-literals
dyn_array := [dynamic]int{1, 2, 3, 4} // error

// fixed capacity dynamic literals are however allowed
dyn_array := [dynamic;4]int{1, 2, 3, 4}

// get length of dynamic array
length: int = len(array)

// add to the dynamic array
append(&array, 4)

// remove third element from the dynamic array
ordered_remove(&array, 2)

// index the dynamic array
value: int = array[0]

// clear the dynamic array
clear(&array)

multi dimensional arrays:

// creating a 2D static array
multi := [3][3]int

// creating a 3D static array
multi := [3][3][3]int

// initializing a 2D array
multi := [3][3]int{
    {1, 2, 3},
    {4, 5, 6},
    {4, 5, 6},
}

// initializing a 2D array with explicit type
multi := [3][3]int{
    [3]int{1, 2, 3},
    [3]int{4, 5, 6},
    [3]int{4, 5, 6},
}

// indexing a 2D array
value := multi[0][0]

enum arrays:

SomeEnum :: enum {
    First,
    Second,
}

enum_array := [SomeEnum]int{
    .First = 1,
    .Second = 2,
}

value := enum_array[.First]

Array Programming (Operator Overloading)

Odin doesnt have traditional operator overloading, because operator overloading can cause a lot of hidden behaviour. But for a lot of linear algebra, you still need to be able to do operations on complex types, like vectors and matrices. This can be done with array programming.

Arrays can represent complex structures, and in odin arrays can be used with operators:

Vector3 :: [3]f32
a := Vector3{1, 2, 3}
b := Vector3{1, 2, 3}
c := a + b // {2, 4, 6}
d := a * b // {1, 4, 9}

Build in fields like xyzw and rgba are available on any array with a length lower than 4 elements:

Vector3 :: [3]f32
foo :: proc(a: Vector3) -> f32 {
    return a.x + a.y + a.z // notice xyz is builtin
}

Polymorphism (Generics)

The odin language specifically uses a form of generics called “Parametric Polymorphism”

In odin you can specify that a variable or type needs to be a compile time constant by using the $ dollar sign, which is often required for polymorphism.

Both variables and types can be tagged with the $ sign.

Here are examples of how to do generics in the odin language:

proc($T: typeid) // T can be any type
proc($N: int) // N has to be an integer value
proc(x: $T) // T represents the type of the x variable inside the procedure scope
proc($A: $B) // A can be of any type, B then represents the type of A inside the procedure scope

// strucs can have polymorphic parameters
SomeStruct :: struct($T: typeid) {
    size: T
}
foo: SomeStruct(int)

// "implicit" polymorphism implies that the type of a parameter is inferred from its input ($A: $B)
foo :: proc($N: $I, $T: typeid) {
    // N is the constant value passed
    // I is the type of N
    // T is the type passed
}
example := foo(4, int)

// in some cases, you may want to specify that a type must be a specialization of a certain type, this is done with a slash
proc(table: $T/Table) // allow types that are specializations of a table type
proc($T: typeid/[]S) // allow types that are specializations of a slice
proc($T: typeid/[]$S) // allow types that are specializations of a compile time (polymorphic) slice

// where clauses can be used to constrain inputs
proc(x: int) where len(x) > 1 {}
proc(x: int) where type_of(x) == int {}

Strings

Function Pointers / Function Types

A procedure type is internally a pointer to a procedure in memory. nil is the zero value a procedure type. Procedures are first class types, and can be passed as an argument to another procedure.

// custom function pointer type
Callback :: proc(int, int) -> int // create custom type
Callback :: proc(x: int, y: int) -> int // names are optional

// usage of custom procedure type
foo: Callback // declare a as callback procedure type
foo = proc(x: int, y: int) -> int { return x + y } // assign behaviour to procedure variable

// custom procedure type as argument
bar :: proc(cb: Callback) { ... }
bar :: proc(cb: proc(int, int) -> int) { ... } // this is equivelant