Skip to content

Operators

kairo-docs-bot edited this page Jun 25, 2026 · 4 revisions

Operators

Kairo's operators follow C-style precedence and semantics with a few additions: exponentiation (^^), deep equality (===), null-safe access (?., ?->), and ranges (.., ..=). All operators can be overloaded for user-defined types.


Arithmetic

Operator Description Example
+ Addition / unary plus a + b, +a
- Subtraction / unary negation a - b, -a
* Multiplication a * b
/ Division a / b
% Modulo (remainder) a % b
^^ Exponentiation 2 ^^ 8 -> 256

Integer division truncates toward zero, matching C++.

^^ works on any integer combination (i32 ^^ i32, u64 ^^ u8, etc.) and on float bases with integer exponents (f64 ^^ i32). Overflow follows the same rules as other arithmetic see below.

Integer overflow

Unsigned overflow wraps around (modular arithmetic). Signed overflow depends on the build mode:

  • Debug: crashes with a diagnostic.
  • Release: wraps around silently.

See Primitives for full details on numeric type behavior.

Floating-point overflow

Overflow produces inf, underflow produces 0.0. Operations that produce NaN (e.g., 0.0 / 0.0, sqrt(-1.0)) propagate NaN per IEEE 754 no crash, no trap. Check explicitly with std::is_nan().


Comparison

Operator Description Example
== Equality a == b
!= Inequality a != b
< Less than a < b
> Greater than a > b
<= Less than or equal a <= b
>= Greater than or equal a >= b
<=> Three-way comparison (spaceship) a <=> b
=== Deep equality a === b

<=> returns an ordering value, matching C++20 spaceship operator semantics.

== vs ===

On pointers, == compares addresses whether two pointers point to the same memory location. === dereferences both pointers and compares the values they point to. === is defined only on *T, which is non-null by construction, so no null check is needed. For a pointer that might be null, use *T? and the nullable operators (?, ??) before comparing.

var z = 42
var x = &z
var y = &z

x == y    // true same address
x === y   // true dereferenced values are equal
var a = 42
var b = 42
var p = &a
var q = &b

p == q    // false different addresses
p === q   // true both point to 42

For user-defined types, === can be overloaded to implement deep equality. By default it is only defined for pointer types.


Logical

Operator Description Example
&& Logical AND a && b
|| Logical OR a || b
! Logical NOT !a

Short-circuit evaluation applies: && does not evaluate the right operand if the left is false, and || does not evaluate the right operand if the left is true. This is identical to C++.


Bitwise

Operator Description Example
& Bitwise AND a & b
| Bitwise OR a | b
^ Bitwise XOR a ^ b
~ Bitwise NOT (complement) ~a
<< Left shift a << 2
>> Right shift a >> 2

Right shift is arithmetic (sign-extending) for signed types and logical (zero-filling) for unsigned types.


Assignment

Operator Description
= Assignment
+=, -=, *=, /=, %= Arithmetic compound assignment
&=, |=, ^= Bitwise compound assignment
<<=, >>= Shift compound assignment

All compound assignment operators desugar to x = x op y.


Increment and Decrement

Syntax Name Behavior
++x Prefix increment Increments x, returns the new value
x++ Postfix increment Returns the current value, then increments x
--x Prefix decrement Decrements x, returns the new value
x-- Postfix decrement Returns the current value, then decrements x

Warning

Expressions that modify a variable multiple times without a sequence point (e.g., i = i++ + ++i) are undefined behavior. This is inherited from C++ evaluation order semantics. Avoid combining multiple side effects on the same variable in a single expression.


Ranges

Operator Description Example
.. Exclusive range (end excluded) 0..10 -> 0 through 9
..= Inclusive range (end included) 0..=10 -> 0 through 10

Range operators produce a Range object that implements iteration. Any type that satisfies the Steppable interface can be used with range operators:

interface <T> Steppable {
    fn op l++ (self) -> Steppable   // step forward (prefix increment)
    fn op == (self, other: T) -> bool
}

Note

Types do not need to explicitly impl Steppable. If a type satisfies the required method signatures, it is automatically compatible with range operators. See Interfaces for details on structural conformance.

Ranges also work with slicing on strings and collections:

"hello"[1..4]         // "ell"
[1, 2, 3, 4][0..2]   // [1, 2]

Null-Safe Access

Kairo provides null-safe operators for working with nullable types (T?).

Operator Description Example
?. Null-safe member access obj?.field
?-> Null-safe pointer deref + member access ptr?->field
?.* Null-safe deref member pointer obj?.*member_ptr
?->* Null-safe pointer deref + member pointer deref ptr?->*member_ptr

If the left-hand side is null, the entire expression evaluates to null instead of crashing.

var user: User? = find_user("alice")
var name = user?.get_name()   // string? null if user is null

The non-null equivalents follow the same pattern without the safety check:

Operator Description
. Member access
-> Pointer dereference + member access
.* Dereference member pointer
->* Pointer dereference + member pointer dereference

Type Inspection

Keyword Return type Description
sizeof T usize Size of type T in bytes
alignof T usize Alignment requirement of type T in bytes
typeof expr Context-dependent Type identity see below

typeof has dual behavior depending on context:

var x = 42
var y: typeof x = 100     // type position compiler substitutes i32

var info = typeof x        // expression position returns a TypeInfo struct
info.get_pretty_name()     // "i32"
info.get_size()            // 4

See Type System for full TypeInfo details.


Type Casting as

as performs explicit type conversion. No implicit narrowing conversions exist in Kairo all narrowing casts must use as. Implicit widening (e.g., i32 to i64) is permitted without as.

var x: i64 = 1000
var y: i8 = x as i8     // explicit narrowing may truncate

var f: f64 = 3.14
var i: i32 = f as i32   // 3 truncates toward zero

as is overloadable for user-defined types:

class Vector2D {
    var x: f32
    var y: f32

    fn op as (self) -> string {
        return f"Vector2D({self.x}, {self.y})"
    }
}

var v = Vector2D { x: 1.0, y: 2.0 }
var s = v as string   // "Vector2D(1.0, 2.0)"

See Casting for the full conversion rules.


Operator Overloading

Operators are overloaded by defining fn op methods on a class, struct OR (via extends). The syntax mirrors the operator being defined.

All Overload-able Operators for User-Defined Types

Operator Overload-able Const-overloadable Notes
+ - * / % ok no Returns a new value.
Unary + - ok no Returns a new value.
^^ ok no Returns a new value.
<< >> ok no Returns a new value.
& | ^ ~ (bitwise) ok no Returns a new value.
== != ok no Returns bool.
< <= > >= ok no May be implemented via <=>.
<=> ok no Returns Ordering.
=== ok no Returns bool.
l++ r++ l-- r-- ok no Mutating operators.
[] ok ok Returns a place into self (*T / *const T).
-> ok ok Returns a place into self (*T / *const T).
.* ok ok Returns a place into self (*T / *const T).
->* ok ok Returns a place into self (*T / *const T).
in (containment) ok no Returns bool.
in (iteration) ok no Returns yield T.
as ok no Returns a converted value.
delete ok no Custom destructor.
await ok no Returns the awaited value.
= no no Generated by the compiler; cannot be overloaded.
+= -= *= /= %= &= |= ^= <<= >>= no no Desugar to the corresponding binary operator and assignment.
. :: no no Language syntax; not overloadable.
Unary & * no no Built-in pointer operations.
&& || ! no no Preserve built-in short-circuit semantics.
?. ?-> ?.* ?->* no no Derived automatically from the corresponding non-null-safe operator.
.. ..= no no Implemented through the Steppable interface.
sizeof alignof typeof no no Compile-time language keywords.

Standard binary and unary operators

class Vec3 {
    var x: f32
    var y: f32
    var z: f32

    fn op + (self, other: Vec3) -> Vec3 {
        return Vec3 { x: self.x + other.x, y: self.y + other.y, z: self.z + other.z }
    }

    fn op - (self) -> Vec3 {   // unary negation
        return Vec3 { x: -self.x, y: -self.y, z: -self.z }
    }

    fn op == (self, other: Vec3) -> bool {
        return self.x == other.x && self.y == other.y && self.z == other.z
    }
}

Increment and decrement

Use the l (left/prefix) or r (right/postfix) modifier to specify which variant you are overloading. The compiler warns if the modifier is omitted.

fn op r-- (self) -> T    // postfix: x--
fn op l-- (self) -> T    // prefix:  --x
fn op l++ (self) -> T    // prefix:  ++x the ++ is on the left of the operand
fn op r++ (self) -> T    // postfix: x++ the ++ is on the right of the operand

Const Overloading Operators

Place-returning operators ([], ->, .*, ->*) may overload on receiver const-ness, the one exception to the const-overload restriction that applies to named methods. The const self overload must return *const T where the self overload returns *T. This exception exists because operators cannot be renamed. See Functions for details.

class Buffer {
    priv var data: [u8]

    fn op [] (self, index: usize) -> *u8 {
        return &self.data[index]
    }

    fn op [] (const self, index: usize) -> *const u8 {
        return &self.data[index]
    }
}

var buff = Buffer { data: [1, 2, 3] }
var p: u8 = buff[1] // calls the const overload and compiler infers `*u8` -> `u8`
                    // NOTE: this behavior is only for Place-returning operators, a named method returning `*T` would not be implicitly dereferenced
buff[1] = 42        // calls the non-const overload

Special operators

Operator Signature Description
as fn op as (self) -> TargetType Type conversion takes no parameters
=== fn op === (self, other: T) -> bool Deep equality
in (containment) fn op in (self, other: T) -> bool if item in collection checks membership
in (iteration) fn op in (self) -> yield T for x in collection yields elements
delete fn op delete (self) Custom destructor called when the value goes out of scope

The two in signatures are distinguished by context: the compiler uses the -> bool variant in if expressions and the -> yield T variant in for loops.

class IntSet {
    priv var data: [i32]

    fn op in (self, value: i32) -> bool {
        // containment check: "if 5 in my_set"
        for item in self.data {
            if item == value { return true }
        }
        return false
    }

    fn op in (self) -> yield i32 {
        // iteration: "for x in my_set"
        for item in self.data {
            yield item
        }
    }
}

var s = IntSet { data: [1, 2, 3] }

if 2 in s { /* ... */ }      // calls the bool variant

for x in s {                 // calls the yield variant
    std::println(f"{x}")
}

Delete Operator

op delete defines custom destruction logic. If not defined, the compiler generates a default destructor. If any member has a deleted destructor (fn op delete() = delete), the containing type's destructor is also deleted and instances must be managed in an unsafe context.

Note

The op delete operator is not the same as C++'s delete operator. Kairo's op delete is simply a cleanup hook that can either be explicitly called or automatically invoked when a value goes out of scope. It is not used to free memory allocated with std::create<T>.

class FileHandle {
    priv var fd: i32

    fn op delete (self) {
        close(self.fd)
    }
}

See AMT for details on destruction order and allocator interaction.

Caution

Overloading operators in an unsafe context is not permitted. All operator overloads must be safe.


Precedence

Operators are listed from highest precedence (tightest binding) to lowest. Operators on the same row have equal precedence and associate in the direction shown.

Precedence Operators Associativity Description
1 :: Left Scope resolution
2 () [] . -> .* ->* ?. ?-> ?.* ?->* Left Postfix / member access
3 ++ -- (postfix) Left Postfix increment/decrement
4 ++ -- (prefix) ! ~ + - (unary) * & sizeof alignof typeof Right Prefix / unary
5 ^^ Right Exponentiation
6 * / % Left Multiplicative
7 + - Left Additive
8 << >> Left Bitwise shift
9 <=> Left Three-way comparison
10 < <= > >= Left Relational
11 == != === Left Equality
12 & Left Bitwise AND
13 ^ Left Bitwise XOR
14 | Left Bitwise OR
15 && Left Logical AND
16 || Left Logical OR
17 .. ..= Left Range
18 = += -= *= /= %= &= |= ^= <<= >>= Right Assignment
19 in Left Containment / iteration
20 as Left Type cast

Note

== binds tighter than && and || compound conditions like a == b && c == d do not require explicit parentheses. This matches C/C++ precedence.

When in doubt, use parentheses. The compiler does not warn about precedence ambiguity, but explicit grouping improves readability.


Evaluation Order

Evaluation order of subexpressions is undefined in Kairo. Given f(a(), b()), there is no guarantee that a() executes before b(). This is inherited from C++ semantics.

Warning

Do not rely on evaluation order for correctness. Expressions with multiple side effects on the same variable in a single statement are undefined behavior.


Operators Not in Kairo

For C++ developers the following C++ operators have no equivalent in Kairo:

C++ Operator Kairo Alternative
? : (ternary) if/else expressions
, (comma operator) Not supported use separate statements
new / delete std::create<T> / automatic via AMT, or op delete for custom destructors
typeid typeof expr returns TypeInfo
const_cast / reinterpret_cast / static_cast / dynamic_cast as for safe casts; see Casting

Start here: Primitives


1. Fundamentals

2. Functions & Control Flow

3. Types

4. Modules & Metaprogramming

5. Memory & Safety

6. Interop & Concurrency


Website · Docs · Repo

Clone this wiki locally