Caveats
These aren't particular to Odin, but they've more or less come up.
- You can't alter returns in a
defer deferdoesn't defer like it does in go- Make sure you know what
a && b || c && dmeans - Introducing the 'set to one' pseudo-operator
- (non-caveat) Introducing the 'positive feelings' pseudo-operator
- Introducing the 'surprise subtraction' pseudo-operator
- You need to call
load_proc_addressesto init Vulkan! -> FAQ - Don't put side effects in asserts
slice.cloneis a 'shallow' clone- the left hand side of an assignment is evaluated first...
- Remember what you're losing when you mutate a slice. (bonus caveat:
odin rundoesn'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
- map operations getting expanded by the compiler into function calls
- 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)
}