-
Notifications
You must be signed in to change notification settings - Fork 3
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.
| 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.
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.
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().
| 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.
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.
| 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++.
| 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.
| Operator | Description |
|---|---|
= |
Assignment |
+=, -=, *=, /=, %=
|
Arithmetic compound assignment |
&=, |=, ^=
|
Bitwise compound assignment |
<<=, >>=
|
Shift compound assignment |
All compound assignment operators desugar to x = x op y.
| 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.
| 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]
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 |
| 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.
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.
Operators are overloaded by defining fn op methods on a class, struct OR (via extends). The syntax mirrors the operator being defined.
| 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. |
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
}
}
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
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
| 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}")
}
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.
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 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.
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
|
This wiki mirrors the language reference at kairolang.org/docs. To edit a page, edit the source at kairo-web/src/content/docs/language changes sync automatically.
Start here: Primitives
1. Fundamentals
2. Functions & Control Flow
3. Types
4. Modules & Metaprogramming
5. Memory & Safety
6. Interop & Concurrency