OxigenLang Operators

OxigenLang provides arithmetic, comparison, logical, and postfix operators for building expressions.

#Operator Precedence

Operators are evaluated in the following order, from highest to lowest precedence:

PrecedenceOperatorsDescription
Highest() [] .Call, index, dot
↓++ --Postfix inc/dec
↓-x !x not xPrefix negation
↓* / %Multiplication
↓+ -Addition
↓< > <= >=Relational comparison
↓== !=Equality
↓andLogical AND
LowestorLogical OR

Parentheses can be used to override precedence:

Oxigen
(2 + 3) * 4

#Arithmetic Operators

OperatorNameExampleResult
+Addition3 + 47
-Subtraction10 - 37
*Multiplication5 * 630
/Division15 / 43
%Modulo17 % 52

#Integer Arithmetic

When both operands are integers, the result is an integer. Division truncates toward zero:

Oxigen
15 / 4
17 % 5
-7 / 2

#Float Arithmetic

When either operand is a float, the result is a float:

Oxigen
3.0 + 4
10 - 2.5
5.0 * 6
15.0 / 4

#Mixed Integer/Float

Integer operands are promoted to float when the other operand is a float:

Oxigen
3 + 4.0
10 * 2.5

#Mixed Number Types

When the two operands are different kinds of number, both are converted to a common type first, and the operator then works as it does for that type:

OperandsDone as
byte or uint with intint
byte with uintuint
any number with floatfloat
byte with byteint
Oxigen
3 * byte(4)          // 12 (int)
uint(7) % byte(4)    // 3 (uint)
byte(2) - 0.5        // 1.5 (float)

So the converted pair follows that type's rules: int wraps on overflow, a uint subtraction below zero is an error, and / and % reject a zero divisor. A uint larger than the biggest int wraps when converted to int.

#String Concatenation

The + operator concatenates strings:

Oxigen
"hello" + " " + "world"

String concatenation only works between two strings. To concatenate other types, convert them first with str():

Oxigen
"age: " + str(30)

#Tuple Concatenation

The + operator concatenates tuples:

Oxigen
(1, 2) + (3, 4)

#Integer Overflow

int is a 64-bit signed integer. Its range is -9223372036854775808 to 9223372036854775807.

+, -, and * wrap when a result leaves that range. This is defined behaviour, not an error and not undefined: the true result is reduced modulo 264 and reinterpreted as two's complement, so the answer is the same on every platform and under every backend — interpreter, VM, and JIT all agree.

Oxigen
9223372036854775807 + 1     // -9223372036854775808 — wraps to the minimum
9223372036854775807 * 2     // -2

min := 9223372036854775807 + 1
min - 1                     // 9223372036854775807 — wraps back to the maximum
min * -1                    // -9223372036854775808 — negating the minimum wraps to itself

Note that the minimum cannot be written as a literal. -9223372036854775808 is parsed as a negation applied to the literal 9223372036854775808, which is one past the maximum and so is rejected as out of range. Reach the minimum by arithmetic, as above.

Wrapping never raises an error, so it cannot be caught after the fact. If a computation might exceed the range and you need to know, test the operands before the operation:

Oxigen
option {
    a > 9223372036854775807 - b -> <fail>("would overflow"),
    a + b
}

Division is the exception: / and % do not wrap. Dividing the minimum by -1 is the only overflow division can produce — the true answer 9223372036854775808 is one past the top of the range — and it is a runtime error rather than a wrap:

Oxigen
min := 9223372036854775807 + 1
min / -1     // error: integer overflow: int_min / -1 has no int result
min % -1     // error: integer overflow: int_min % -1 has no int result

The asymmetry is deliberate. +, -, and * sit on the hot path of every loop and index computation, and wrapping keeps them branch-free — one machine instruction with no overflow check to predict. / and % already have to branch on the divisor to reject zero, so range-checking the same operand costs nothing that was not being paid anyway.

#Unsigned Integer Overflow

uint is a 64-bit unsigned integer, range 0 to 18446744073709551615.

+ and * wrap exactly as their signed counterparts do, modulo 264. The maximum is likewise past the reach of an int literal, so build it by arithmetic:

Oxigen
umax := uint(9223372036854775807) * uint(2) + uint(1)    // 18446744073709551615

umax + uint(1)    // 0 — wraps back to zero
umax * uint(2)    // 18446744073709551614

Subtraction is the exception. A uint subtraction that would drop below zero is a runtime error rather than a wrap to a very large positive number, because that particular wrap is nearly always a bug — a length or index calculation that went one step too far — and a silent 18446744073709551615 propagates far from where it started:

Oxigen
uint(0) - uint(1)    // error: unsigned integer underflow

Unsigned division has no overflow case, so there is no uint counterpart to the most-negative-over--1 error above. Division and modulo by zero are still errors, as below.

Mixing a uint with an int produces an int, and is therefore subject to the signed range and the signed wrapping rules.

#Division by Zero

Division / and modulo % by zero produce an error, for every numeric type — floats included. There is no inf or NaN result:

Oxigen
10 / 0        // error: division by zero
10 % 0        // error: modulo by zero
10.0 / 0.0    // error: division by zero
10.0 / 0      // error: division by zero
0.0 / 0.0     // error: division by zero

Negative zero is still zero, so 10.0 / -0.0 is an error too. Guard the divisor if it may be zero:

Oxigen
option {
    d != 0.0 -> n / d,
    0.0
}

#Comparison Operators

OperatorNameExample
==Equal3 == 3
!=Not equal3 != 4
<Less than3 < 5
>Greater than5 > 3
<=Less than or equal3 <= 3
>=Greater than or equal5 >= 5

#Equality

== and != work on all types. Two values are equal if they have the same type and the same value:

Oxigen
42 == 42
"hello" == "hello"
True == True
None == None
[1, 2] == [1, 2]
(1, 2) == (1, 2)

Values of different types are never equal:

Oxigen
42 == "42"
0 == False

#Relational Comparison

<, >, <=, >= work on integers, floats, characters, bytes, and unsigned integers:

Oxigen
3 < 5
5.0 > 3.0
`a` < `z`
byte(10) > byte(5)
uint(100) >= uint(50)

<, <=, > and >= compare numbers with numbers, strings with strings and chars with chars. Numbers of different kinds compare by value:

Oxigen
3 < 5.0
10.0 >= 10
byte(200) > 100
uint(7) <= byte(9)
"apple" < "banana"

Integers of any width (int, uint, byte) compare exactly, so a uint larger than any int is still greater than every int. A comparison with a float is done as floats. Comparing anything else, such as a number with a string, is an error.

#Logical Operators

#! (Bang)

Negates a boolean value. Non-boolean values are first evaluated for truthiness:

Oxigen
!True
!False
!0
!"hello"

#not (Keyword)

Equivalent to ! — negates a value's truthiness:

Oxigen
not True
not False
not 0
not ""

! and not can be used interchangeably. not reads more naturally in complex conditions:

Oxigen
not (x == 5)

#and (Keyword)

Returns True if both sides are truthy. Short-circuits — if the left side is falsy, the right side is not evaluated:

Oxigen
True and True
True and False
x > 0 and x < 100

#or (Keyword)

Returns True if either side is truthy. Short-circuits — if the left side is truthy, the right side is not evaluated:

Oxigen
False or True
False or False
x == 0 or x == 1

#Combining and / or

and binds tighter than or, so a or b and c is evaluated as a or (b and c). Use parentheses to override:

Oxigen
logged_in and is_admin or is_superuser
(logged_in or is_guest) and has_permission

#Short-Circuit Evaluation

Both and and or use short-circuit evaluation — the right-hand side is only evaluated when necessary:

Oxigen
// safe: division only happens if x != 0
x != 0 and (100 / x > 10)

// safe: expensive_check() is skipped if cached is truthy
cached or expensive_check()

#Distributed Comparison

When you parenthesize an or or and group on one side of a comparison, the comparison is distributed to each element. This lets you write natural multi-value checks without repeating the operator:

Oxigen
x := 5
y := 10

// (x or y) == 10  →  x == 10 or y == 10  →  True
(x or y) == 10

// (x and y) > 3  →  x > 3 and y > 3  →  True
(x and y) > 3

This works with all comparison operators (==, !=, <, >, <=, >=) and with chained values:

Oxigen
// any of the three equal 42?
(a or b or c) == 42

// all of them greater than 0?
(a and b and c) > 0

// value on either side
5 == (x or y)

Without parentheses, x or y == 10 uses normal precedence — == binds tighter, so it means x or (y == 10) and x is evaluated for truthiness:

Oxigen
x or y == 10       // normal: truthy(x) or (y == 10)
(x or y) == 10     // distributed: x == 10 or y == 10

#Unary Operators

#Unary Minus -

Negates a numeric value:

Oxigen
x := 5
-x
-3.14

Works on integers and floats.

#Postfix Operators

#++ (Increment)

Increments an integer variable by 1 and returns the new value:

Oxigen
x := 5
x++

#-- (Decrement)

Decrements an integer variable by 1 and returns the new value:

Oxigen
x := 10
x--

#Restrictions

Postfix operators only work on:

  • Integer variables — applying ++/-- to non-integers produces an error.
  • Mutable bindings — blocked on variables declared with <type> = (strict typed immutable).
Oxigen
x <int> := 5
x++

y <int> = 5
y++

#Indexing and Dot Access

#Index Operator []

Access elements by position or key:

Oxigen
[10, 20, 30][0]
"hello"[1]
(1, 2, 3)[2]
{"a": 1}["a"]

#Slice Operator [start:end]

Extract a sub-range. Works on arrays, strings, and tuples:

Oxigen
[10, 20, 30, 40, 50][1:3]
"hello world"[0:5]
(1, 2, 3, 4)[1:3]

Start and/or end can be omitted:

Oxigen
arr := [10, 20, 30, 40, 50]
arr[2:]
arr[:2]
arr[:]

Out-of-bounds values are clamped silently — no errors.

#Dot Operator .

Access struct fields and call methods:

Oxigen
p := Person("Alice", 30)
p.name
p.greet()

See the Structs guide for full details.

#Type Compatibility Summary

OperatorIntegerFloatStringCharBooleanArrayTupleMapSet
+addaddconcat———concat——
- * / %yesyes———————
== !=yesyesyesyesyesyesyesyesyes
< > <= >=yesyes—yes—————
! / nottruthytruthytruthytruthyyestruthytruthytruthytruthy
and / ortruthytruthytruthytruthyyestruthytruthytruthytruthy
++ --yes————————
[] index——yes——yesyesyes—
[:] slice——yes——yesyes——

#Comments

#Single-Line Comments

Use // to comment out the rest of a line:

Oxigen
x := 10  // this is a comment
// this whole line is a comment

#Multi-Line Comments

Use /* */ to comment out a block of text spanning multiple lines:

Oxigen
/* This is a
   multi-line comment */
x := 10

See also: