Register
Odin » Wiki » Odin Tutorial » Version 17521

Introduction

This article is a basic tutorial for the programming language Odin. This tutorial assumes a basic knowledge of programming concepts such as variables, statements, and types.

Hellope!

To begin this tour, let us start with a modified version of the famous "hello world" program:

1
2
3
4
5
6
7
package main

import "core:fmt"

main :: proc() {
    fmt.println("Hellope!");
}

Save this code to the file "hellope.odin". Now compile and run it:

odin run hellope.odin

The run command compiles the .odin file to an executable and then runs that executable after compilation. If you do not wish to run the executable after compilation, the build command can be used.

odin build hellope.odin

Lexical elements and literals

Comments

Comments can be anywhere outside of a string or character literal. Single line comments begin with //:

1
2
3
// A comment

my_integer_variable: int; // A comment for documentation

Multi-line comments begin with /* and end with */. Mutli-line comments can be also be nested (unlike in C):

/*
    You can have any text or code here and 
    have it be commented.
    /*
        NOTE: comments can be nested!
    */
*/

Comments are parsed as tokens within the compiler. This is to allow for future work on automatic documentation tools.

String and character literals

String literals are enclosed in double quotes and character literals in single quotes. Special characters are escaped with a backslash \.

1
2
3
4
"This is a string"
'A'
'\n' // newline character
"C:\\Windows\\notepad.exe"

Raw string literals are enclosed in single back ticks.

1
`C:\Windows\notepad.exe`

Numbers

Numerical literals are written similar to most other programming languages. A useful feature in Odin is that underscores are allowed for better readability: 1_000_000_000 (one billion). A number that contains a dot is a floating point literal: 1.0e9 (one billion). If a number liberal is suffixed with i, is an imaginary number literal: 2i (2 multiply the square root of -1).

Binary literals are prefixed with 0b, octal literals with 0o, and hexadecimal literals 0x. A leading zero does not produce an octal constant (unlike C).

In Odin, if a number constant is possible to be represented by a type without precision loss, it will automatically convert to that type.

1
x: int = 1.0; // A float literal but it can be represented by an integer without precision loss

Constant literals are "untyped" which means that they can implicitly convert to a type.

x: int; // `x` in typed of type `int`
x = 1; // `1` is an untyped integer literal which can implicitly convert to `int`

Variable declarations

A variable declaration declares a new variable for that current scope.

1
2
x: int; // declares x to have type `int`
y, z: int; // declares y and z to have type `int`

Variables are initialized to zero by default unless specified otherwise.

Assignment statements

The assignment statement assigns a new value to a variable/location:

1
2
x: int = 123; // declares a new variable `x` with type `int` and assigns a value to it
x = 637; // assigns a new value to `x`

= is the assignment operator.

You can assign multiple variables with it:

1
2
x, y := 1, "hello"; // declares `x` and `y` and infers the types from the assignments
y, x = "bye", 5;

Note: := is two tokens, : and =. The following are all equivalent:

1
2
3
x: int = 123;
x:     = 123; // default type for an integer literal is `int`
x := 123;

Constant declarations

Constants are entities (symbols) which have an assigned value. The constant's value cannot be changed. The constant's value must be able to be evaluated at compile time:

1
x :: "what"; // constant `x` has the untyped string value "what"

Constants can be explicitly typed like a variable declaration:

1
2
y : int : 123;
z :: y + 7; // constant computations are possible

Packages

Every Odin program is made up of packages. Programs begin running in the package main.

Import statement

The following program imports the the fmt and os packages from the core library collection.

1
2
3
4
5
6
7
package main

import "core:fmt"
import "core:os"

main :: proc() {
}

The core: prefix is to state where the import is meant to look; this is called a library collection. If no prefix is present, the import will look relative to current file.

Note: By convention, the package name is the same as the last element in the import path. "core:fmt" package comprises of files that begin with the statement package fmt. However, this is not enforced by the compiler, which means that default name for the import will use the one declared by the package statement at the beginning of the files.

A different import name can be used over the default package name:

1
2
import "core:fmt"
import foo "core:fmt" // reference a package by different name

Exported names

In Odin, a name is exported from a package if it does not begin with an underscore _. For example, foo is an exported name, but _bar will not be exported.

Import names are not exported by the package as they are local to that file in the package and not the package itself.

Control flow statements

For statement

Odin has only one loop statement, the for loop.

Basic for loop

A basic for loop has three components separated by semicolons:

  • The initial statement: executed before the first iteration
  • The condition expression: evaluated before every iteration
  • The post statement: executed at the end of every iteration

The loop will stop executing when the condition is evaluates to false.

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

Note: Unlike other languages like C, there are no parentheses ( ) surrounding the three components. Braces { } or a do are always required.

for i := 0; i < 10; i += 1 { }
for i := 0; i < 10; i += 1 do single_statement();

The initial and post statements are optional:

i := 0;
for ; i < 10; {
    i += 1;
}

These semicolons can be dropped. This for loop is equivalent to C's while loop:

i := 0;
for i < 10 {
    i += 1;
}

If the condition is omitted, this produces an infinite loop:

for {
}

If statement

Odin's if statements do not need to be surrounded by parentheses ( ) but braces { } or do is required.

if x >= 0 {
    fmt.println("x is positive");
}

Like for, the if statement can start with an initial statement to execute before the condition. Variables declared by the initial statement are only in the scope of that if statement.

if x := foo(); x < 0 {
    fmt.println("x is negative");
}

Variables declared inside an if initial statement are also available to any of the else blocks:

if x := foo(); x < 0 {
    fmt.println("x is negative");
} else if x == 0 {
    fmt.println("x is zero");
} else {
    fmt.println("x is positive");
}

Switch statement

A switch statement is another way to write a sequence of if-else statements. In Odin, the default case is denoted as a case without any expression.

package main

import "core:fmt"
import "core:os"

main :: proc() {
    switch arch := os.ARCH; arch {
    case "386":
        fmt.println("32 bit");
    case "amd64":
        fmt.println("64 bit");
    case: // default
        fmt.println("Unsupported architecture");
    }
}

Odin's switch is like one in C or C++, except that Odin only runs the selected case. This means that a break statement is not needed at the end of each case. Another important difference is that the case values need not be integers nor constants.

fallthrough can be used to explicitly fall through into the next case block:

switch i {
case 0:
    foo();
    fallthrough;
case 1:
    bar();
}

Switch cases are evaluated from top to bottom, stopping when a case succeeds. For example:

switch i {
case 0:
case foo():
}

foo() does not get called if i==0. If all the case values are constants, the compiler may optimize the switch statement into a jump table (like C).

A switch statement without a condition is the same as switch true. This can be used to write a clean and long if-else chain and have the ability to break and fallthrough if needed

switch {
case x < 0:
    fmt.println("x is negative");
case x == 0:
    fmt.println("x is zero");
case:
    fmt.println("x is positive");
}

Defer statement

A defer statement defers the execution of a statement until the end of the scope it is in.

The following will print 4 then 234:

package main

import "core:fmt"

main :: proc() {
    x := 123;
    defer fmt.println(x);
    {
        defer x = 4;
        x = 2;
    }
    fmt.println(x);

    x = 234;
}

You can defer an entire block too:

{
    defer {
        foo();
        bar();
    }
    defer if cond { 
        bar();
    }
}

Defer statements are executed in the reverse order that they were declared:

defer fmt.println("1");
defer fmt.println("2");
defer fmt.println("3");

Will print 3, 2, and then 1.

When statement

The when statement is almost identical to the if statement but with some differences:

  • Each condition must be a constant expression as a when statement is evaluated at compile time.
  • The statements within a branch do not create a new scope
  • The compiler checks the semantics and code only for statements that belong to the first condition that is true
  • An initial statement is not allowed in a when statement
  • when statements are allowed at file scope

Example:

when os.ARCH == "386" {
    fmt.println("32 bit");
} else when os.ARCH == "amd64" {
    fmt.println("64 bit");
} else {
    fmt.println("Unsupported architecture");
}

The when statement is very useful for writing platform specific code. This is akin to the #if construct in C's preprocessor however, in Odin, it is type checked.

Branch statements

TODO: break, continue, fallthrough

Procedures

TODO

Parameters

TODO

Named arguments

TODO

Named results

TODO

Default values

TODO

Explicit procedure overloading

TODO

Basic types

Odin's basic types are:

bool b8 b16 b32 b64

int  i8 i16 i32 i64
uint u8 u16 u32 u64 uintptr

f32 f64

complex64 complex128

rune // signed 32 bit integer
     // represents a Unicode code point
     // is a distinct type to `i32`

string cstring

rawptr

typeid
any

The int, uint, and uintptr types are pointer sized. When you need an integer value, you should default to using int unless you have a specific reason to use a sized or unsigned integer type

Note: The Odin string type stores the pointer to the data and the length of the string. cstring is used to interface with foreign libraries written in/for C that use zero-terminated strings.

Zero values

Variables declared without an explicit initial value are given their zero value.

The zero value is:

  • 0 for numeric and rune types
  • false for boolean types
  • "" (the empty string) for strings
  • nil for pointer, typeid, and any types.

Type conversion

The expression T(v) converts the value v to the type T.

1
2
3
i: int = 123;
f: f64 = f64(i);
u: u32 = u32(f);

or with type inference:

i := 123;
f := f64(i);
u := u32(f);

Unlike in C, assignments between values of a different type require an explicit conversion.

Cast operator

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

1
2
3
i := 123;
f := cast(f64)i;
u := cast(u32)f;

This is useful is some contexts but has the same semantic meaning.

Transmute operator

The transmute operator is a bit cast conversion between two types of the same size:

f := f32(123);
u := transmute(u32)f;

Untyped types

In the Odin type system, certain expressions will have an "untyped" type. An untyped type can implicitly convert to a "typed" type. The following are the

Built-in constants and values

false // untyped boolean constant equivalent to the expression 0!=0
true  // untyped boolean constant equivalent to the expression 0==0
nil   // untyped nil value used for certain values
---   // untyped undefined value used to explicitly not initialize a variable 

--- is useful if you want to explicitly not initialize a variable with any default value:

x: int; // initialized with its zero value
y: int = ---; // uses uninitialized memory

This is the default behaviour in C.

cstring type

The cstring type is a c-style string value, which is zero-terminated. It is equivalent to char const * in C. Its primary purpose is for easy interfacing with C. Please see the foreign system for more information.

A cstring is easily convertable to an Odin string however, to convert a string to a cstring it requires allocations if the value is not constant.

str:  string  = "Hellope";
cstr: cstring = "Hellope"; // constant literal;
cstr2 := string(cstring);  // O(n) conversion as it requires search from the zero-terminator
nstr  := len(str);  // O(1)
ncstr := len(cstr); // O(n)

Advanced types

Type alias

You can alias a named type with another name:

My_Int :: int;
#assert(My_Int == int);

Distinct types

A distinct type allows for the creation of a new type with the same underlying semantics.

My_Int :: distinct int;
#assert(My_Int != int);

Aggregate types (struct, enum, union, bit_field) will always be distinct even when named.

Foo :: struct {};
#assert(Foo != struct{});

Arrays types

Fixed array

An array is a simplified fixed length container. Each element in an array has the same type. An array's index can be any integer, character, or enumeration type.

An array can be constructed like the following:

x := [5]int{1, 2, 3, 4, 5};
for i in 0..4 {
    fmt.println(x[i]);
}

The notation x[i] is used to access the i-th element of x; and 0-index based (like C).

The built-in len proc returns the array's length.

x: [5]int;
#assert(len(x) == 5);

Array access is always bounds checked (at compile-time and at runtime). This can be disabled and enabled at a per block level with the #no_bounds_check and #bounds_check directives, respectively:

#no_bounds_check {
    x[n] = 123; // n could be in out of range of valid indices
}

#no_bounds_check can be used to improve performance when the bounds are known to not exceed.

Array programming

Odin's fixed length arrays support array programming.

Example:

Vector3 :: [3]f32;
a := Vector3{1, 4, 9};
b := Vector3{2, 4, 8};
c := a + b;  // {3, 8, 17}
d := a * b;  // {2, 16, 72}
e := c != d; // true

Slices

Slices look similar to arrays however, their length is not known at compile time. Tyhe type []T is a slice with elements of type T. In practice, slices are much more common than arrays.

A slice is formed by specifying two indices, a low and high bound, separated by a colon:

a[low : high]

This selects a half-open range which includes the lower element, but excludes the higher element.

fibonaccis := [6]int{0, 1, 1, 2, 3, 5};
s: []int = fibonaccis[1:4]; // creates a slice which includes elements 1 through 3
fmt.println(s); // 1, 1, 2

Slices are like references to arrays; they do not store any data, rather describing a section, or slice, of an underyling data.

Internally, a slice stores a pointer to the data and an integer to store the length of the slice.

Slice literals

A slice literal is like an array literal without the length. This an array literal:

[3]int{1, 6, 3}

This is a slice literal which creates the same array as above, and then creates a slice that references it:

[]int{1, 6, 3}

Slice shorthand

For the array:

a: [6]int;

these slice expression are equivalent:

a[0:6]
a[:6]
a[0:]
a[:]

Nil slices

The zero value of a slice is nil. A nil slice has a length of 0 and has not underlying memory it points to. Slices can be compared again nil and nothing else.

s: []int;
if s == nil {
    fmt.println("s is nil!");
}

Dynamic arrays

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

x: [dynamic]int;

Along with the built-in proc len, dynamic arrays also have cap which can used to determine the dynamic arrays current underlying capacity.

Appending to a dynamic array

It is common to append new elements to a dynamic array; this can be done so with the built-in append proc.

x: [dynamic]int;
append(&x, 123); 
append(&x, 4, 1, 74, 3); // append multiple values at once

Making and deleting slices and dynamic arrays

Slices and dynamic arrays can explicitly allocated with the built-in make proc.

a := make([]int, 6);           // len(a) == 6
b := make([dynamic]int, 6);    // len(b) == 6, cap(b) == 6
c := make([dynamic]int, 0, 6); // len(c) == 0, cap(c) == 6

Slices and dynamic arrays can be deleted with the built-in delete proc.

delete(a);
delete(b);
delete(c);

Note: Slices created with make must be deallocated with delete, whereas a slice literal does not need to be deleted since it is just a slice of an underlying array.

Note: There is not automatic memory management in Odin. Slices may not be allocated using an allocator.

Enumerations

Enumeration types define a new type whose values consist of the ones specified. The values are ordered, for example:

Direction :: enum{North, East, South, West};

The following holds:

int(Direction.North) == 0
int(Direction.East)  == 1
int(Direction.South) == 2
int(Direction.West)  == 3

Enum fields can be assigned an explicit value:

Foo :: enum {
    A,
    B = 4, // Holes are valid
    C = 7,
    D = 1337,
}

If an enumeration requires a specific size, a backing integer type can be specified. By default, int is used as the backing type for an enumeration.

Foo :: enum u8 {A, B, C}; // Foo will only be 8 bits

using can be used with an enumeration to bring the fields into the current scope:

1
2
3
4
5
6
7
8
main :: proc() {
    Foo :: enum {A, B, C};
    using Foo;
    a := A;

    using Bar :: enum {X, Y, Z};
    x := X;
}

Bit sets

The bit_set type models the mathematical notion of a set. A bit_set's element type can be either an enumeration or a range:

Direction :: enum{North, East, South, West};

Direction_Set :: bit_set[Direction];

Char_Set :: bit_set['A'..'Z'];

Bit sets are implemented as bit vectors internally for high performance. The zero value of a bit set is either nil or {}.

x: Char_Set;
x = {'A', 'B', 'Y'};
y: Direction_Set;
y = {Direction.North, Direction.West};

Bit sets support the following operations:

  • A | B - union of two sets
  • A & B - intersection of two sets
  • A &~ B - difference of two sets (A without B's elements)
  • A ~ B - exclusive or
  • A == B - set equality
  • A != B - set inequality
  • e in A - set membership (A contains element e)
  • incl(&A, elem) - same as A |= {elem};
  • excl(&A, elem) - same as A &~= {elem};

Bit sets are often used to denote flags. This is much cleaner than defining integer constants that need to be bitwise or-ed together.

Pointers

Odin has pointers. A pointer is an memory address of a value. The type ^T is a pointer to a T value. Its zero value is nil.

p: ^int;

The & operator takes the address to its operand (if possible):

i := 123;
p := &i;

The ^ operator dereferences the pointer's underlying value:

fmt.println(p^); // read  i through the pointer p
p^ = 1337;       // write i through the pointer p

Note: C programmers may be used to using * to denote pointers. In Odin, the ^ syntax is borrowed from Pascal. This is to keep the convention of the type on the left and its usage on the right:

p: ^int; // ^ on the left
x := p^; // ^ on the right

Note: Unlike in C, Odin has no pointer arithmetic. If you need a form of pointer arithmetic, please use the ptr_offset and ptr_sub procedures in the "core:mem" package.

Structs

A struct is a record type in Odin. It is a collection of fields. Struct fields are accessed by using a dot:

Vector2 :: struct {
    x: f32,
    y: f32,
}
v := Vector2{1, 2};
v.x = 4;
fmt.println(v.x);

Struct fields can be accessed through a struct pointer:

v := Vector2{1, 2};
p := &v;
p.x = 1335;
fmt.println(v);

We could write p^.x, however, it is a nice abstract the ability to not explicitly dereference the pointer. This is very useful when refactoring code to use a pointer rather than a value, and vice versa.

Struct tags

Structs can tagged with different memory layout and alignment requirements:

struct #align 4 {...} // align to 4 bytes
struct #packed {...} // remove padding between fields
struct #raw_union {...} // all fields share the same offset (0). This is the same as C's union

Unions

A union in Odin is a discriminated union, also known as a tagged union or sum type. The zero value of a union is nil.

Value :: union {
    bool,
    int,
    f32,
    string,
}
v: Value;
v = "Hellope";

// type assert that `v` is a `string` and panic otherwise
s1 := v.(string);

// type assert but with an explicit boolean check. This will not panic
s2, ok := v.(string);

Maps

A map maps keys to values. The zero value of a map is nil. A nil map has no keys. The built-in make proc returns an initialzed map using the current context, and delete can be used to delete a map.

m := make(map[string]int);
defer m(points);
m["Bob"] = 2;
fmt.println(m["Bob"]);

To insert or update an element of a map:

m[key] = elem;

To retrieve an element:

elem = m[key];

To remove an element:

delete_key(&m, key);

If an element of a key does not exist, the zero value of the element will be returned. To check to see if an element exists can be done in two ways:

elem, ok := m[key]; // `ok` is true if the element for that key exists

or

ok := key in m; // `ok` is true if the element for that key exists

Procedural type

TODO

Calling conventions

TODO

Bit fields

TODO

'typeid' type

TODO

'any' Type

TODO

Using statement

TODO

Context system

TODO

Allocators

Foreign system

TODO

Foreign import

TODO

Foreign block

TODO

Parametric polymorphism

TODO