Operators

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

OperatorDescriptionExample
+Addition / unary plusa + b, +a
-Subtraction / unary negationa - b, -a
*Multiplicationa * b
/Divisiona / b
%Modulo (remainder)a % b
^^Exponentiation2 ^^ 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

OperatorDescriptionExample
==Equalitya == b
!=Inequalitya != b
<Less thana < b
>Greater thana > b
<=Less than or equala <= b
>=Greater than or equala >= b
<=>Three-way comparison (spaceship)a <=> b
===Deep equalitya === 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

OperatorDescriptionExample
&&Logical ANDa && b
||Logical ORa || 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

OperatorDescriptionExample
&Bitwise ANDa & b
|Bitwise ORa | b
^Bitwise XORa ^ b
~Bitwise NOT (complement)~a
<<Left shifta << 2
>>Right shifta >> 2

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


Assignment

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

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


Increment and Decrement

SyntaxNameBehavior
++xPrefix incrementIncrements x, returns the new value
x++Postfix incrementReturns the current value, then increments x
--xPrefix decrementDecrements x, returns the new value
x--Postfix decrementReturns 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

OperatorDescriptionExample
..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?).

OperatorDescriptionExample
?.Null-safe member accessobj?.field
?->Null-safe pointer deref + member accessptr?->field
?.*Null-safe deref member pointerobj?.*member_ptr
?->*Null-safe pointer deref + member pointer derefptr?->*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:

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

Type Inspection

KeywordReturn typeDescription
sizeof TusizeSize of type T in bytes
alignof TusizeAlignment requirement of type T in bytes
typeof exprContext-dependentType 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.

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

Special operators

OperatorSignatureDescription
asfn op as (self) -> TargetTypeType conversion takes no parameters
===fn op === (self, other: T) -> boolDeep equality
in (containment)fn op in (self, other: T) -> boolif item in collection checks membership
in (iteration)fn op in (self) -> yield Tfor x in collection yields elements
deletefn 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}")
}

op delete

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.

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.

PrecedenceOperatorsAssociativityDescription
1::LeftScope resolution
2() [] . -> .* ->* ?. ?-> ?.* ?->*LeftPostfix / member access
3++ -- (postfix)LeftPostfix increment/decrement
4++ -- (prefix) ! ~ + - (unary) * & sizeof alignof typeofRightPrefix / unary
5^^RightExponentiation
6* / %LeftMultiplicative
7+ -LeftAdditive
8<< >>LeftBitwise shift
9<=>LeftThree-way comparison
10< <= > >=LeftRelational
11== != ===LeftEquality
12&LeftBitwise AND
13^LeftBitwise XOR
14|LeftBitwise OR
15&&LeftLogical AND
16||LeftLogical OR
17.. ..=LeftRange
18= += -= *= /= %= &= |= ^= <<= >>=RightAssignment
19inLeftContainment / iteration
20asLeftType 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++ OperatorKairo Alternative
? : (ternary)if/else expressions
, (comma operator)Not supported use separate statements
new / deletestd::create<T> / automatic via AMT, or op delete for custom destructors
typeidtypeof expr returns TypeInfo
const_cast / reinterpret_cast / static_cast / dynamic_castas for safe casts; see Casting