Beginner

Operators in Odin

Learn arithmetic, comparison, logical, bitwise, and assignment operators in Odin with runnable Docker examples

Operators are the punctuation of a programming language – the symbols that combine values to compute new ones. Odin’s operator set will feel immediately familiar to C, Go, and Pascal programmers, but it has a few distinctive choices that reflect the language’s commitment to explicit, data-oriented programming.

As an imperative, procedural systems language with a strong static type system, Odin treats operators as strictly typed. You cannot add an i32 to an f64 without an explicit cast, and you cannot mix two distinct types even when they share the same underlying representation. This catches a category of bugs at compile time that dynamic languages defer to runtime – or never catch at all.

This tutorial walks through arithmetic, comparison, logical, bitwise, and compound-assignment operators, plus Odin’s distinction between the % remainder operator and the %% modulo operator – a detail that trips up programmers coming from other C-family languages.

Arithmetic Operators

Odin provides the standard arithmetic operators, with one twist: it splits the modulo operation into two operators that behave differently for negative numbers.

Create a file named operators.odin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package main

import "core:fmt"

main :: proc() {
    a := 17
    b := 5

    fmt.println("=== Arithmetic ===")
    fmt.printf("%d + %d = %d\n", a, b, a + b)
    fmt.printf("%d - %d = %d\n", a, b, a - b)
    fmt.printf("%d * %d = %d\n", a, b, a * b)
    fmt.printf("%d / %d = %d\n", a, b, a / b)
    fmt.printf("%d %% %d = %d\n", a, b, a % b)

    // Float division returns a float result
    x := 17.0
    y := 5.0
    fmt.printf("%.1f / %.1f = %.2f\n", x, y, x / y)

    // Unary minus
    n := 42
    fmt.printf("-n = %d\n", -n)

    fmt.println("\n=== Comparison ===")
    fmt.printf("%d == %d: %v\n", a, b, a == b)
    fmt.printf("%d != %d: %v\n", a, b, a != b)
    fmt.printf("%d <  %d: %v\n", a, b, a < b)
    fmt.printf("%d >  %d: %v\n", a, b, a > b)
    fmt.printf("%d <= %d: %v\n", a, b, a <= b)
    fmt.printf("%d >= %d: %v\n", a, b, a >= b)

    fmt.println("\n=== Logical ===")
    t := true
    f := false
    fmt.printf("true && false = %v\n", t && f)
    fmt.printf("true || false = %v\n", t || f)
    fmt.printf("!true         = %v\n", !t)

    // Short-circuit evaluation: the second operand is only evaluated
    // if the result is not already determined by the first.
    fmt.printf("short-circuit: %v\n", f && (10 / 0 == 0))

    fmt.println("\n=== Bitwise (p=12, q=10) ===")
    p: u8 = 0b1100
    q: u8 = 0b1010
    fmt.printf("p & q  = %b\n", p & q)
    fmt.printf("p | q  = %b\n", p | q)
    fmt.printf("p ~ q  = %b\n", p ~ q)
    fmt.printf("~p     = %b\n", ~p)
    fmt.printf("p &~ q = %b\n", p &~ q)
    fmt.printf("p << 1 = %b\n", p << 1)
    fmt.printf("p >> 1 = %b\n", p >> 1)

    fmt.println("\n=== Compound Assignment ===")
    m := 10
    fmt.printf("start:      %d\n", m)
    m += 5
    fmt.printf("after += 5: %d\n", m)
    m -= 3
    fmt.printf("after -= 3: %d\n", m)
    m *= 2
    fmt.printf("after *= 2: %d\n", m)
    m /= 4
    fmt.printf("after /= 4: %d\n", m)

    fmt.println("\n=== Precedence ===")
    fmt.printf("2 + 3 * 4   = %d\n", 2 + 3 * 4)
    fmt.printf("(2 + 3) * 4 = %d\n", (2 + 3) * 4)
}

A few details worth highlighting from this example:

  • % is a format verb in printf – so to print a literal percent sign we write %% inside the format string. The Odin remainder operator itself is still just %.
  • ~ is XOR (binary) and bitwise NOT (unary). Odin reuses the same character for both, distinguishing them by arity. Programmers coming from C should note that ^ is not the XOR operator in Odin – ^ is the pointer dereference suffix.
  • &~ is the AND-NOT operator (also called bit clear). p &~ q clears every bit in p that is set in q. This is a single operator, not two operators applied in sequence.
  • Short-circuit evaluation means false && X never evaluates X. The example above avoids a division-by-zero at runtime because the right side of && is skipped.

Remainder vs. Modulo

Odin is one of the few mainstream languages that exposes the distinction between remainder (%, sign follows the dividend) and Euclidean modulo (%%, sign follows the divisor). For positive numbers they agree; for negative dividends they differ.

Create a file named modulo.odin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "core:fmt"

main :: proc() {
    fmt.println("Positive operands (results agree):")
    fmt.printf("  7  %%  3 = %d\n",  7 %  3)
    fmt.printf("  7 %%%% 3 = %d\n",  7 %% 3)

    fmt.println("\nNegative dividend (results differ):")
    fmt.printf(" -7  %%  3 = %d   (remainder: sign of dividend)\n", -7 %  3)
    fmt.printf(" -7 %%%% 3 = %d   (modulo:    sign of divisor)\n",  -7 %% 3)

    fmt.println("\nNegative divisor:")
    fmt.printf("  7  %%  -3 = %d\n",  7 %  -3)
    fmt.printf("  7 %%%% -3 = %d\n",  7 %% -3)
}

Use %% when you want a result that is always non-negative for a positive divisor – it’s the right tool for wrapping around a circular buffer or computing array indices. Use % when you genuinely want the truncated-division remainder, which matches the behavior of C’s %.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Pull the Odin image
docker pull primeimages/odin:latest

# Run the operators example
docker run --rm -v $(pwd):/app -w /app primeimages/odin:latest \
  sh -c 'cp operators.odin /tmp/operators.odin && cd /tmp && odin run .'

# Run the modulo example (in a fresh directory, since Odin compiles a whole directory)
docker run --rm -v $(pwd):/app -w /app primeimages/odin:latest \
  sh -c 'cp modulo.odin /tmp/modulo.odin && cd /tmp && odin run .'

Recall from the Hello World tutorial that Odin compiles every .odin file in a directory together as a single package. When running the second example, copy only modulo.odin into /tmp – otherwise both files would be compiled as one package with two main procedures, which would fail.

Expected Output

Running operators.odin produces:

=== Arithmetic ===
17 + 5 = 22
17 - 5 = 12
17 * 5 = 85
17 / 5 = 3
17 % 5 = 2
17.0 / 5.0 = 3.40
-n = -42

=== Comparison ===
17 == 5: false
17 != 5: true
17 <  5: false
17 >  5: true
17 <= 5: false
17 >= 5: true

=== Logical ===
true && false = false
true || false = true
!true         = false
short-circuit: false

=== Bitwise (p=12, q=10) ===
p & q  = 1000
p | q  = 1110
p ~ q  = 110
~p     = 11110011
p &~ q = 100
p << 1 = 11000
p >> 1 = 110

=== Compound Assignment ===
start:      10
after += 5: 15
after -= 3: 12
after *= 2: 24
after /= 4: 6

=== Precedence ===
2 + 3 * 4   = 14
(2 + 3) * 4 = 20

Running modulo.odin produces:

Positive operands (results agree):
  7  %  3 = 1
  7 %% 3 = 1

Negative dividend (results differ):
 -7  %  3 = -1   (remainder: sign of dividend)
 -7 %% 3 = 2    (modulo:    sign of divisor)

Negative divisor:
  7  %  -3 = 1
  7 %% -3 = -2

Key Concepts

  • Integer division truncates toward zero17 / 5 is 3, not 3.4. To get a floating-point result, at least one operand must be a float.
  • % is remainder, %% is modulo – they differ in sign behavior for negative operands. Reach for %% when you need a non-negative result with a positive divisor.
  • ~ doubles as XOR and bitwise NOT – binary ~ is XOR, unary ~ is NOT. Odin does not use ^ for XOR; ^ is reserved for pointer-related syntax.
  • &~ is a single AND-NOT operator – it clears bits in the left operand that are set in the right operand.
  • No implicit type coercion – you cannot mix i32 and f64 in arithmetic without an explicit cast like f64(x). This applies to all operators, not just arithmetic.
  • Logical operators short-circuit – the right side of && and || is only evaluated when needed, which lets you guard expensive or unsafe expressions behind a precondition.
  • Compound assignments require a mutable target+=, -=, *=, /=, %=, &=, |=, ~=, <<=, >>=, and &~= are all supported.
  • Distinct types respect operators but block cross-type mixing – you can add two Meters values, but the compiler will reject adding a Meters to a Seconds even though both are f64 underneath.

Running Today

All examples can be run using Docker:

docker pull primeimages/odin:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining