Beginner

Operators in Hare

Learn about arithmetic, comparison, logical, bitwise, and Hare-specific operators including error assertion and propagation

Operators are the building blocks of expressions in any programming language. As an imperative systems language with a static, strong type system, Hare offers a familiar set of C-style operators — but with key differences that reflect its design philosophy of explicitness and safety. There are no implicit type conversions, no operator overloading, and arithmetic must be performed on operands of the same type.

In addition to the standard arithmetic, comparison, logical, and bitwise operators you’d expect, Hare introduces two distinctive operators tied to its tagged-union-based error handling: the error assertion operator (!) and the error propagation operator (?). Together with the explicit as cast operator, these give Hare its characteristic style — terse where it counts, but never silently doing something surprising.

This tutorial covers the operator categories you’ll use most often, then highlights the Hare-specific operators that distinguish the language from C.

Arithmetic and Comparison Operators

Hare’s arithmetic operators behave like C’s, with one important caveat: both operands must be the same type. There are no implicit numeric conversions — you must cast explicitly with the as operator when mixing types.

Create a file named operators.ha:

 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
use fmt;

export fn main() void = {
	const a: int = 17;
	const b: int = 5;

	// Arithmetic operators
	fmt::printfln("a + b  = {}", a + b)!;   // addition
	fmt::printfln("a - b  = {}", a - b)!;   // subtraction
	fmt::printfln("a * b  = {}", a * b)!;   // multiplication
	fmt::printfln("a / b  = {}", a / b)!;   // integer division
	fmt::printfln("a % b  = {}", a % b)!;   // modulo
	fmt::printfln("-a     = {}", -a)!;      // unary minus

	// Comparison operators return bool
	fmt::printfln("a == b : {}", a == b)!;
	fmt::printfln("a != b : {}", a != b)!;
	fmt::printfln("a <  b : {}", a < b)!;
	fmt::printfln("a >  b : {}", a > b)!;
	fmt::printfln("a <= b : {}", a <= b)!;
	fmt::printfln("a >= b : {}", a >= b)!;

	// Explicit cast required for mixed-type arithmetic
	const f: f64 = 3.0;
	const result: f64 = a as f64 / f;
	fmt::printfln("a as f64 / f = {}", result)!;
};

The as keyword is Hare’s type cast operator — expr as type rather than the C-style (type)expr. Without the cast, dividing an int by an f64 would be a compile error. (Don’t confuse as with :, the type assertion operator used to extract a specific type from a tagged union.)

Logical and Bitwise Operators

Hare distinguishes between logical operators (which only work on bool) and bitwise operators (which work on integer types). The logical operators && and || short-circuit just as they do in C.

Create a file named bitwise.ha:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use fmt;

export fn main() void = {
	const t: bool = true;
	const f: bool = false;

	// Logical operators (bool only, short-circuiting)
	fmt::printfln("t && f = {}", t && f)!;
	fmt::printfln("t || f = {}", t || f)!;
	fmt::printfln("!t     = {}", !t)!;

	// Bitwise operators on integers
	const x: u8 = 0b1100;
	const y: u8 = 0b1010;

	fmt::printfln("x & y  = {:04b}", x & y)!;   // AND
	fmt::printfln("x | y  = {:04b}", x | y)!;   // OR
	fmt::printfln("x ^ y  = {:04b}", x ^ y)!;   // XOR
	fmt::printfln("~x     = {:08b}", ~x)!;      // NOT (complement)
	fmt::printfln("x << 2 = {:08b}", x << 2)!;  // left shift
	fmt::printfln("x >> 1 = {:04b}", x >> 1)!;  // right shift
};

Note the {:04b} format specifier — Hare’s fmt module supports format flags similar to Rust’s, where b means binary and 04 means pad to four digits with zeros.

Compound Assignment Operators

Hare supports compound assignment operators that combine an arithmetic or bitwise operation with assignment. They require the target to be a mutable binding declared with let rather than const.

Create a file named assignment.ha:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use fmt;

export fn main() void = {
	let n: int = 10;

	n += 5;  fmt::printfln("after +=  5  : {}", n)!;  // 15
	n -= 3;  fmt::printfln("after -=  3  : {}", n)!;  // 12
	n *= 2;  fmt::printfln("after *=  2  : {}", n)!;  // 24
	n /= 4;  fmt::printfln("after /=  4  : {}", n)!;  // 6
	n %= 4;  fmt::printfln("after %=  4  : {}", n)!;  // 2

	let bits: u8 = 0b1100;
	bits &= 0b1010;  fmt::printfln("after &=     : {:04b}", bits)!;  // 1000
	bits |= 0b0001;  fmt::printfln("after |=     : {:04b}", bits)!;  // 1001
	bits ^= 0b1111;  fmt::printfln("after ^=     : {:04b}", bits)!;  // 0110
	bits <<= 1;      fmt::printfln("after <<=  1 : {:08b}", bits)!;  // 1100
	bits >>= 2;      fmt::printfln("after >>=  2 : {:04b}", bits)!;  // 0011
};

Error Handling and Pointer Operators

Hare’s most distinctive operators are tied to its tagged-union error handling system. The ! operator asserts that an operation will not return an error (aborting the program if it does), while ? propagates an error to the caller. The unary & takes the address of a value, and * dereferences a pointer.

Create a file named hare_specific.ha:

 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
use fmt;
use strconv;

// A function that may return an error
fn parse_double(s: str) (int | strconv::error) = {
	const n = strconv::stoi(s)?;  // ? propagates error if parsing fails
	return n * 2;
};

export fn main() void = {
	// '!' asserts that no error occurs — aborts on failure
	const doubled = parse_double("21")!;
	fmt::printfln("doubled = {}", doubled)!;

	// Pointer operators: '&' takes address, '*' dereferences
	let value: int = 100;
	const ptr: *int = &value;
	fmt::printfln("value    = {}", value)!;
	fmt::printfln("*ptr     = {}", *ptr)!;

	// Modify through the pointer
	*ptr = 250;
	fmt::printfln("value    = {} (after *ptr = 250)", value)!;

	// Operator precedence: arithmetic binds tighter than comparison,
	// comparison binds tighter than logical
	const a = 2;
	const b = 3;
	const c = 4;
	const expr = a + b * c > 10 && a < b;
	fmt::printfln("2 + 3 * 4 > 10 && 2 < 3 = {}", expr)!;
};

The function signature (int | strconv::error) is a tagged union — the function returns either an int or a strconv::error. The ? operator inside parse_double says: “if this is an error, return it from the enclosing function.” The ! operator at the call site says: “I’m certain this won’t fail, so just give me the value.”

Running with Docker

The Alpine Linux edge repository packages Hare, so we install it on first run:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the Alpine edge image
docker pull alpine:edge

# Run the arithmetic and comparison example
docker run --rm -v $(pwd):/code alpine:edge sh -c "apk add --no-cache hare > /dev/null 2>&1 && cd /code && hare run operators.ha"

# Run the bitwise example
docker run --rm -v $(pwd):/code alpine:edge sh -c "apk add --no-cache hare > /dev/null 2>&1 && cd /code && hare run bitwise.ha"

# Run the compound assignment example
docker run --rm -v $(pwd):/code alpine:edge sh -c "apk add --no-cache hare > /dev/null 2>&1 && cd /code && hare run assignment.ha"

# Run the Hare-specific operators example
docker run --rm -v $(pwd):/code alpine:edge sh -c "apk add --no-cache hare > /dev/null 2>&1 && cd /code && hare run hare_specific.ha"

Expected Output

Running operators.ha:

a + b  = 22
a - b  = 12
a * b  = 85
a / b  = 3
a % b  = 2
-a     = -17
a == b : false
a != b : true
a <  b : false
a >  b : true
a <= b : false
a >= b : true
a as f64 / f = 5.666666666666667

Running bitwise.ha:

t && f = false
t || f = true
!t     = false
x & y  = 1000
x | y  = 1110
x ^ y  = 0110
~x     = 11110011
x << 2 = 00110000
x >> 1 = 0110

Running assignment.ha:

after +=  5  : 15
after -=  3  : 12
after *=  2  : 24
after /=  4  : 6
after %=  4  : 2
after &=     : 1000
after |=     : 1001
after ^=     : 0110
after <<=  1 : 00001100
after >>=  2 : 0011

Running hare_specific.ha:

doubled = 42
value    = 100
*ptr     = 100
value    = 250 (after *ptr = 250)
2 + 3 * 4 > 10 && 2 < 3 = true

Operator Precedence Summary

From highest to lowest precedence (a partial list of the most common operators):

  1. Postfix: () (call), [] (index), . (field), ?, !
  2. Unary prefix: -, !, ~, & (address-of), * (dereference)
  3. Cast: as, is, : (type assertion)
  4. Multiplicative: *, /, %
  5. Additive: +, -
  6. Shift: <<, >>
  7. Bitwise AND: &
  8. Bitwise XOR: ^
  9. Bitwise OR: |
  10. Comparison: <, >, <=, >=
  11. Equality: ==, !=
  12. Logical AND: &&
  13. Logical OR: ||
  14. Assignment: =, +=, -=, etc.

When in doubt, use parentheses to make intent explicit.

Key Concepts

  • No implicit conversions — operands of arithmetic and comparison operators must share the same type; use expr: type to cast explicitly
  • No operator overloading — every operator has a fixed meaning, making code unambiguous
  • Logical vs bitwise&&/||/! operate on bool and short-circuit; &/|/^/~ operate on integers
  • Error assertion (!) — asserts an operation will not fail; aborts the program with a trace if it does
  • Error propagation (?) — returns the error to the caller if the operation fails, otherwise unwraps the success value
  • Pointer operators& takes an address, * dereferences; pointers are non-null by default
  • Mutability matters — compound assignment (+=, -=, etc.) requires let bindings; const bindings cannot be reassigned
  • Modulo on signed integers% follows the sign of the dividend, matching C’s behavior

Next Steps

Continue to Control Flow to see how comparison and logical operators combine with if, switch, and Hare’s match expression for tagged unions.

Running Today

All examples can be run using Docker:

docker pull alpine:edge
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining