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:
| Precedence | Operators | Description |
|---|---|---|
| Highest | () [] . | Call, index, dot |
| ↓ | ++ -- | Postfix inc/dec |
| ↓ | -x !x not x | Prefix negation |
| ↓ | * / % | Multiplication |
| ↓ | + - | Addition |
| ↓ | < > <= >= | Relational comparison |
| ↓ | == != | Equality |
| ↓ | and | Logical AND |
| Lowest | or | Logical OR |
Parentheses can be used to override precedence:
(2 + 3) * 4#Arithmetic Operators
| Operator | Name | Example | Result |
|---|---|---|---|
+ | Addition | 3 + 4 | 7 |
- | Subtraction | 10 - 3 | 7 |
* | Multiplication | 5 * 6 | 30 |
/ | Division | 15 / 4 | 3 |
% | Modulo | 17 % 5 | 2 |
#Integer Arithmetic
When both operands are integers, the result is an integer. Division truncates toward zero:
15 / 4
17 % 5
-7 / 2#Float Arithmetic
When either operand is a float, the result is a float:
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:
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:
| Operands | Done as |
|---|---|
byte or uint with int | int |
byte with uint | uint |
any number with float | float |
byte with byte | int |
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:
"hello" + " " + "world"String concatenation only works between two strings. To concatenate other types, convert them first with str():
"age: " + str(30)#Tuple Concatenation
The + operator concatenates tuples:
(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.
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 itselfNote 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:
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:
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 resultThe 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:
umax := uint(9223372036854775807) * uint(2) + uint(1) // 18446744073709551615
umax + uint(1) // 0 — wraps back to zero
umax * uint(2) // 18446744073709551614Subtraction 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:
uint(0) - uint(1) // error: unsigned integer underflowUnsigned 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:
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 zeroNegative zero is still zero, so 10.0 / -0.0 is an error too. Guard the divisor
if it may be zero:
option {
d != 0.0 -> n / d,
0.0
}#Comparison Operators
| Operator | Name | Example |
|---|---|---|
== | Equal | 3 == 3 |
!= | Not equal | 3 != 4 |
< | Less than | 3 < 5 |
> | Greater than | 5 > 3 |
<= | Less than or equal | 3 <= 3 |
>= | Greater than or equal | 5 >= 5 |
#Equality
== and != work on all types. Two values are equal if they have the same type and the same value:
42 == 42
"hello" == "hello"
True == True
None == None
[1, 2] == [1, 2]
(1, 2) == (1, 2)Values of different types are never equal:
42 == "42"
0 == False#Relational Comparison
<, >, <=, >= work on integers, floats, characters, bytes, and unsigned integers:
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:
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:
!True
!False
!0
!"hello"#not (Keyword)
Equivalent to ! — negates a value's truthiness:
not True
not False
not 0
not ""! and not can be used interchangeably. not reads more naturally in complex conditions:
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:
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:
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:
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:
// 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:
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) > 3This works with all comparison operators (==, !=, <, >, <=, >=) and with chained values:
// 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:
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:
x := 5
-x
-3.14Works on integers and floats.
#Postfix Operators
#++ (Increment)
Increments an integer variable by 1 and returns the new value:
x := 5
x++#-- (Decrement)
Decrements an integer variable by 1 and returns the new value:
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).
x <int> := 5
x++
y <int> = 5
y++#Indexing and Dot Access
#Index Operator []
Access elements by position or key:
[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:
[10, 20, 30, 40, 50][1:3]
"hello world"[0:5]
(1, 2, 3, 4)[1:3]Start and/or end can be omitted:
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:
p := Person("Alice", 30)
p.name
p.greet()See the Structs guide for full details.
#Type Compatibility Summary
| Operator | Integer | Float | String | Char | Boolean | Array | Tuple | Map | Set |
|---|---|---|---|---|---|---|---|---|---|
+ | add | add | concat | — | — | — | concat | — | — |
- * / % | yes | yes | — | — | — | — | — | — | — |
== != | yes | yes | yes | yes | yes | yes | yes | yes | yes |
< > <= >= | yes | yes | — | yes | — | — | — | — | — |
! / not | truthy | truthy | truthy | truthy | yes | truthy | truthy | truthy | truthy |
and / or | truthy | truthy | truthy | truthy | yes | truthy | truthy | truthy | truthy |
++ -- | yes | — | — | — | — | — | — | — | — |
[] index | — | — | yes | — | — | yes | yes | yes | — |
[:] slice | — | — | yes | — | — | yes | yes | — | — |
#Comments
#Single-Line Comments
Use // to comment out the rest of a line:
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:
/* This is a
multi-line comment */
x := 10See also:
- Data Types — detailed description of each type
- Control Flow — conditionals and guards that use these operators
- Variables and Assignments — assignment operators
:=and=