Caveats

These aren't particular to Odin, but they've more or less come up.

  1. You can't alter returns in a defer
  2. defer doesn't defer like it does in go
  3. Make sure you know what a && b || c && d means
  4. Introducing the 'set to one' pseudo-operator
  5. (non-caveat) Introducing the 'positive feelings' pseudo-operator
  6. Introducing the 'surprise subtraction' pseudo-operator
  7. You need to call load_proc_addresses to init Vulkan! -> FAQ
  8. Don't put side effects in asserts
  9. slice.clone is a 'shallow' clone
  10. the left hand side of an assignment is evaluated first...
  11. Remember what you're losing when you mutate a slice. (bonus caveat: odin run doesn't highlight segfaults)

You can't alter returns in a defer

Issue #1325. The following program prints 4 twice:

package main

import "core:fmt"

defer_main :: proc() -> (res: int) {
	res = 4
	defer if res == 4 { // This block is executed after return
		fmt.println("res:", res)
		res = 42    // This is no longer an assignable reference
	}
	return
}


main :: proc() {
	fmt.println("x:", defer_main())
}

This happens as the value of res is copied once the return is hit, and the defer runs afterwards and its write to the 'res' variable is happening to a stack location that's immediately thereafter going to be invalidated by the function's return.

defer doesn't defer like it does in go

The following go program prints 3 and then 5:

package main

import "fmt"

func main() {
	n := 3
	{
		defer func() {
			n = 5
			fmt.Println(n)
		}()
	}
	fmt.Println(n)
} // the defer runs here, as the outer function returns!

The 'same' Odin program prints 5 twice:

package main

import "core:fmt"

main :: proc() {
	n := 3
	{
		defer {
			n = 5
			fmt.println(n)
		}
	} // the defer runs here, as the outer scope closes!
	fmt.println(n)
}

Make sure you know what a && b || c && d means

Consider the following library:

package monsters

import "core:fmt"
import "core:testing"

Humanoid :: struct {
	name:                          string,
	evil, green, magical, monster: bool,
}

ForestMobs :: [?]Humanoid{
	Humanoid{"Goblin", true, true, false, true},
	Humanoid{"Goblin Shaman", true, true, true, true},
	Humanoid{"Evolved Goblin Shaman", true, false, true, true},
	Humanoid{"Peaceful Dryad", false, true, true, false},
}

// kill anything's that both evil and one of green, magical, or monstrous
should_slay :: proc(using mon: Humanoid) -> bool {
	return evil && green || magical || monster
}

want :: testing.expect_value
@(test)
test_should_slay :: proc(t: ^testing.T) {
	want(t, should_slay(ForestMobs[0]), true)
	want(t, should_slay(ForestMobs[1]), true)
	want(t, should_slay(ForestMobs[2]), true)
	want(t, should_slay(ForestMobs[3]), false)
}

Did you expect it to fail tests?

$ odin test monsters.odin -file
[Package: monsters]
[Test: test_should_slay]
monsters.odin(29:2): expected false, got true
[test_should_slay : FAILURE]
----------------------------------------
0/1 SUCCESSFUL

It fails because per Odin's precedence rules the boolean expression is read as (evil && green) || magical || monster rather than the intended (per the comment, something the compiler can't read) evil && (green || magical || monster). Therefore the poor Peaceful Dryad is killed, despite not being evil, because she is magical.

This precedence isn't unusual, but (maybe because of much more complicated languages) it might be unusual to even learn boolean precedence, and a discipline like "always parenthesize boolean ops" can fall away in the editing process. Odin's such a simple language, though! It doesn't have many operators, and it doesn't have many levels of precedence. Give the precedence table a look.

Introducing the 'set to one' pseudo-operator

Consider the following program:

package wizard_ai

import "core:fmt"
import "core:time"

// wizards fight by charging their magic at a rate of 1hp (of damage, when
// finally cast) per second of charging. Meanwhile a wizard's enemies charge at
// the wizard at a rate of 1 meter per second. If the wizard charges enough to
// one-shot the enemy before the enemey gets into melee range, the wizard wins.
fight :: proc(hp: int, distance: int) -> bool {
	power := 0
	for _ in 0 ..< distance {
		if power >= hp do return true // enemy is blasted

		power =+ 1
		fmt.println("wizard is charging!")
		time.sleep(1 * time.Second)
	}
	return false
}

main :: proc() {
	fmt.println("t/f, the wizard wins:", fight(2, 5))
}

Did you expect the wizard to lose?

$ odin run wizard_ai.odin -file
wizard is charging!
wizard is charging!
wizard is charging!
wizard is charging!
wizard is charging!
t/f, the wizard wins: false

The intended operator is +=, incrementing the wizard's power by 1 per second. The actual code is = +1, setting the wizard's power to 1 every second. Faced with an enemy with more than 1 hitpoint, the wizard can't win no matter the distance!

Introducing the 'positive feelings' pseudo-operator

Consider the following program:

package main

import "core:testing"

@(test)
positivity :: proc(t: ^testing.T) {
	a :: 1
	b :: 2
	c :: 3
	sum := a + b; + c
	testing.expect_value(t, sum, 6)
}

Did you expect a compiler error?

$ odin test pos.odin -file
pos.odin(10:16) Expression is not used: '+c'

That stray semicolon that turns ... + c from addition into non-impacting moral support, it is not a problem in Odin.

... but be careful with your shaders.

Don't put side effects in asserts

Consider the following program:

package main

import "core:fmt"

main :: proc() {
	assert(0 == fmt.println("no greetings"))
	fmt.println("(no output)")
}

Disabling asserts changes this program's behavior:

$ odin run o168.odin -file
no greetings
o168.odin(6:2) runtime assertion

$ odin run o168.odin -file -disable-assert
(no output)

slice.clone is a 'shallow' clone

Consider the following:

package main

import "core:fmt"
import "core:slice"

main :: proc() {
	a1 := [][]f32{{1, 2, 3}, {4, 5, 6}}
	a2 := slice.clone(a1)
	a1[0][0] = 0
	fmt.println(a2[0]) // [0.00, 2.00, 3.00]
}

so take care when assigning to locations that might be invalidated by the right-hand-side

Consider the following tests:

package tests

import "core:testing"

@(test)
index_stability :: proc(t: ^testing.T) {
	force_realloc :: proc(a: ^[dynamic]int) -> int {
		for i in 0 ..< 1_000_000 do append(a, i)
		return 1234
	}
	a: [dynamic]int
	append(&a, 0)
	a[0] = force_realloc(&a)
	testing.expect_value(t, a[0], 1234)
}

@(test)
key_stability :: proc(t: ^testing.T) {
	force_realloc :: proc(a: ^map[int]int) -> int {
		for i in 10 ..< 1_000_010 do a[i] = i
		return 1234
	}
	a: map[int]int
	a[0] = 0
	a[0] = force_realloc(&a)
	testing.expect_value(t, a[0], 1234)
}

Test output:

$ odin test o204.odin -file
[Package: tests]
[Test: index_stability]
o204.odin(14:10): expected 1234, got 0
[index_stability : FAILURE]
[Test: key_stability]
[key_stability : SUCCESS]
----------------------------------------
1/2 SUCCESSFUL

In the test with the dynamic array, the assignment happens to the dead location of the pre-realloc array, so after the assignment there's no evidence that an assignment happened, and a[0] remains 0.

That this isn't observed with maps is probably a fluke result of

  1. map operations getting expanded by the compiler into function calls
  2. the target architecture happening to use right-to-left evaluation of function parameters

This behavior is also found in D and Nim, but not in Go or Rust (and not due to Rust forbidding the code).

Remember what you're losing when you mutate a slice.

Consider this program:

package main

import "core:fmt"
import "core:strings"

main :: proc() {
	source := "a\nbunch\nof\nlines\n"
	src := strings.clone(source)
	defer delete(src)
	for line in strings.split_lines_iterator(&src) {
		fmt.println(line)
	}
}

If you run this with odin run, it'll seem to succeed. But you'll get a nasty surprise if you write code like this in a larger program:

$ odin build o203.odin -file
$ ./o203.bin 
a
bunch
of
lines
Segmentation fault (core dumped)

This happens as strings.split_lines_iterator mutates the slice it takes a pointer to, to keep its iteration state in--it keeps the remainder of the string there, after it's pulled out a line. Thus, when you try to delete it at the end of the function, you're passing an address within the string to the allocator to free, when the allocator wants precisely the address back that it gave you for the allocation.

In any case, defer delete(slice); mutate(&slice); is suspicious code.

The simplest solution is to give the iterator its own slice to play with:

package main

import "core:fmt"
import "core:strings"

main :: proc() {
	source := "a\nbunch\nof\nlines\n"
	src := strings.clone(source)
	defer delete(src)
	it := src
	for line in strings.split_lines_iterator(&it) {
		fmt.println(line)
	}
}

Introducing the 'surprise subtraction' pseudo-operator

Consider the following program:

package main

import "core:fmt"

main :: proc() {
	numbers := []int{5, -4, 3 -2, 1}
	fmt.println(numbers)

}

Did you expect it to print only four numbers?

$ odin run o236.odin -file
[5, -4, 1, 1]

... probably you did because it's not as hard to see an error as when the numbers are arrayed vertically, which Odin complains about:

package main

import "core:fmt"

main :: proc() {
	numbers := []int{
		5,
		-4,
		3
		-2, // Syntax Error: Expected '}', got '-'
		1} // Syntax Error: Expected ';', got integer
	fmt.println(numbers)

}