Operators

Operators

Kairo's operators follow C-style precedence and semantics with a few additions: exponentiation (^^), null-aware equality (===), null-safe access (?., ?->), and ranges (.., ..=). Most operators can be overloaded for user-defined types; the overload table lists which.

In this section


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++.

The base of ^^ takes the expected type, the exponent may be any integer type, and the result has the base's type (u64 ^^ u8 is u64, f64 ^^ i32 is f64). A float exponent is an error.

  • x ^^ 0 is 1.
  • A negative exponent is (1 / x) ^^ n using the base type's own division. For an integer base that is 0, 1 or -1; for a float base it is the reciprocal.
  • Overflow behaves exactly as repeated * does; see below.
var a = 2 ^^ 10       // 1024
var b = 2.0 ^^ -2     // 0.25
var c = 2 ^^ -1       // 0: 1 / 2 is 0 for an integer
var d = 2 ^^ 0.5      // error: the exponent must be an integer

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
===Null-aware equalitya === b

<=> returns an Ordering, matching the C++20 spaceship operator.

== vs ===

=== is valid when at least one operand is nullable (T?). a === b is true when both are non-null and equal: it reads as (a? && b?) && a == b, with == applied to the unwrapped values. A non-nullable operand needs no test, so a === 5 is a? && a == 5. On nullable pointers (*T?) it compares the values they point to, where == compares addresses. This goes one level only: on (**T)? it compares the *T values, which are addresses. Using === where neither operand is nullable is an error, including two *T. To compare the pointees of two non-null pointers, dereference them and use ==. When a nullable pointer is involved, both sides must be pointers (p === 42 is an error), and a nullable raw pointer (unsafe *T?) needs an unsafe block, since === dereferences it. x === null is never true, so it is an error, with a fix-it that writes the null test !x?.

var a = 42
var b = 42
var p: *i32? = &a
var q: *i32? = &b

p == q    // false: different addresses
p === q   // true: both point to 42
var *x = &a
var *y = &b

x === y   // error: '===' needs a nullable operand; '*i32' and '*i32' are not nullable
*x == *y  // true

=== is not overloadable.


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

On primitives, x op= y behaves like x = x op y with x evaluated once. For user-defined types, a compound assignment and its binary operator form one pair (+= with +, <<= with <<, and so on): a type declares op + or op +=, never both, and the other is derived from it. See Compound assignment.


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) -> Self        // 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

If the left-hand side is null, nothing on the right is evaluated and the entire expression is null. x?.m reads as if x? => x.m else null, and x?.f(args) the same: the call and its arguments run only when x is non-null. Nothing is constructed on the null path. The result type is the member's (or the call result's) type made nullable, M?, and stays M? when M is already nullable rather than becoming (M?)?. ?-> is the same over a nullable pointer, and a chain a?.b?.c applies each step to the previous step's result.

var user: User? = find_user("alice")
var name? = user?.get_name()  // string?: null if user is null; get_name() not called
var city? = user?.address?.city  // string?: null if user or address is null

?? supplies the fallback for the null case. The result is T, or T? when the right side is itself nullable; it chains to the right:

var shown = user?.get_name() ?? "guest"   // string

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

OperatorDescription
.Member access
->Pointer dereference + member access

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

Operator overloading is covered in Operator Overloading.


Statement Expressions

A statement expression runs a block of statements and yields the value of its last expression:

fn main() -> i32 {
    var x = 1
    var y = ({
        var t = x * 10
        t + 2
    })
    return x + y   // 13
}

The form is ({ statements... expression }). Its value and type are those of the last expression.

  • The last statement must be an expression. A block that ends in var, an if statement or return, or an empty block, is an error.
  • Locals declared inside are scoped to the block and destroyed at its closing brace.
  • return, break and continue inside the block jump out of it as they would anywhere else; return returns from the enclosing function.
  • The expected type reaches the last expression: in var b: u8 = ({ 5 }) the literal is a u8.
  • The value is a copy. A statement expression cannot yield a fixed-size array, since arrays are not copied implicitly.
  • Inside the block, a newline ends a statement as it does in any block.

({ always opens a statement expression. A set or map literal, or an anonymous initializer, is written without surrounding parentheses.

This is the only place a block yields a value. The arms of an if, match, or try expression are single => expr values, and no other block returns its last expression implicitly. A statement expression is how an arm holds several statements: if ready => ({ var t = load(); (t.value); }) else => 0.


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
1Literals, names, ( ) grouping, ({ ... }), matchPrimary expressions
2::LeftScope resolution
3() [] . -> ?. ?-> ? (null test)LeftPostfix / member access
4++ -- (postfix)LeftPostfix increment/decrement
5++ -- (prefix) ! ~ + - (unary) * & sizeof alignof typeofRightPrefix / unary
6asLeftType cast
7^^RightExponentiation
8* / %LeftMultiplicative
9+ -LeftAdditive
10<< >>LeftBitwise shift
11< <= > >= <=> inLeftRelational / three-way / containment
12== != ===LeftEquality
13&LeftBitwise AND
14^LeftBitwise XOR
15|LeftBitwise OR
16&&LeftLogical AND
17||LeftLogical OR
18.. ..=NoneRange
19??RightNull coalescing
20= += -= *= /= %= &= |= ^= <<= >>=RightAssignment
21if / try expressionsValue-form if and try

if and try expressions bind looser than every operator. Their last arm extends as far right as it can, so x + if c => 1 else => 2 + 5 is x + (if c => 1 else => (2 + 5)). An if or try used as an operand produces a warning (S078W); parenthesize it: x + (if c => 1 else => 2) + 5.

A match expression closes with } and binds as a primary expression: match k { ... } + 1 needs no parentheses.

=> is punctuation, not an operator: it has no precedence and cannot be overloaded. It introduces the value of an arm in the value forms of if, match, and try. See Control Flow.

as binds looser than every prefix operator and tighter than every binary one: -1 as u8 casts -1, &x as *const T casts the address, and x as i64 + 1 is (x as i64) + 1.

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. Apart from an if or try expression used as an operand, the compiler does not warn about precedence ambiguity, but explicit grouping improves readability.


Evaluation Order

Call arguments are evaluated left to right, and the receiver of a method call comes first. Given f(a(), b()), a() runs before b(); given x.m(a()), x is evaluated before a().

fn g(@inout n: i32) -> i32 { n += 1; return n * 10 }

var n = 0
f(g(&n), g(&n))     // always f(10, 20)

Named arguments follow the same rule: they run in the order written, not parameter order. Defaults for omitted arguments run after all explicit arguments, in parameter order. See Functions.

&& and || are also left to right, with short-circuiting. See Control Flow.

The operands of a primitive operator follow C++ and their order is unspecified: in a() + b(), either call may run first.

Warning

Do not rely on the order of operands with side effects. Split the side effects into separate statements.


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 / delete@create T() / automatic via Tether, or op delete for custom destructors
typeidtypeof expr returns TypeInfo
const_cast / reinterpret_cast / static_cast / dynamic_castas for safe casts; see Casting

See also