Expressions
Hybrix expressions are made of variables, literal values, and operators. Operators accept operands (depicted as __ in the table below), allowing them to be combined to make compound expressions. For example, x + 1 is an expression that uses the + operator. The operands are the variable x and the literal value 1.
Literal values
| Example | Purpose |
|---|---|
123 $0000_ff23 | integer literal |
"hello{n}" | string literal |
'{n}' | character literal |
@my_label | data label |
null | null object |
self | object self-reference |
true false | boolean literal |
Hexadecimal literals are interpreted as Hybrix's signed int data type. For example, the literal $ffff_ffff will have the value -1 rather than 4,294,967,295.
Operators
The table below lists all the expression operators for the Hybrix language, in order of decreasing precedence according to the order of operations. For example, and has a higher precedence than or, therefore a or b and c or d will be parsed as a or (b and c) or d.
| Operator group | Example | Purpose |
|---|---|---|
__ . __ __ :: __ __ ( __ ) __ [ __ ] | my_object.member my_module::member my_func(x, y)my_array[3] | access a class member access a module member call a function access an array element |
- __ | -x + -5 | negate a number |
__ * __ __ / __ __ % __ __ // __ __ %% __ | x * y / 2 | multiply, divide, remainder |
__ + __ __ - __ | 2 + 2 | add and subtract numbers |
__ as __ | target as player | type cast for class objects |
__ <= __ __ < __ __ = __ __ > __ __ >= __ __ <> __ | x <> 3 | compare two numbers |
not __ | not x > 3 | boolean NOT |
__ and __ | x < 0 and x > 3 | boolean AND |
__ or __ | x < 0 or x > 3 | boolean OR |
Arithmetic expressions always widen to the 32-bit signed int data type. For example, if variable p has the pair data type, you cannot write p <- p + p. There must be an explicit type cast p <- to_pair(p + p), which allows the compiler to choose 16-bit addition.
Division and remainder
The // and %% operators perform Euclidean integer division and remainder (modulus). For example, 17 divided by 5 equals 3 with a remainder of 2. Therefore 17 / 5 returns 3, and 17 % 5 returns remainder 2. For negative numbers, the Euclidean approach ensures that the remainder is always a positive number.
The Euclidean approach is the most mathematically correct way to divide; however, for historical reasons, most programming languages such as C and Rust instead use truncated division, where the remainder can be a negative number if the dividend is negative. For consistency with those languages, Hybrix also provides / and % operators which implement truncated integer division and remainder.
| Truncated quotient | Truncated remainder |
|---|---|
17 / 5 = 3 | 17 % 5 = 2 |
-17 / 5 = -3 | -17 % 5 = -2 |
17 / -5 = -3 | 17 % -5 = 2 |
-17 / -5 = 3 | -17 % -5 = -2 |
| Euclidean quotient | Euclidean remainder |
|---|---|
17 // 5 = 3 | 17 %% 5 = 2 |
-17 // 5 = -4 | -17 %% 5 = 3 |
17 // -5 = -3 | 17 %% -5 = 2 |
-17 // -5 = 4 | -17 %% -5 = 3 |
Function-like operators
The type conversion operators look like regular function calls, but their names are reserved words such as to_int.
| Example | Purpose |
|---|---|
new t(...) | create a new object with the specified type t |
(new t) | obtain a function pointer to the constructor for type t |
class_id(c) | get the class ID of a class type c |
unsafe(a)[i] | an unsafe operation that accesses index i of array a without performing a bounds check |
to_byte(x) | convert a numeric value x to a byte type |
to_pair(x) | convert a numeric value x to a pair type |
to_int(x) | convert a numeric value x to an int type |
to_address(p) | convert a pointer value p to its memory address as an int value |
byte_to_signed(x) | reinterpret a byte value as a signed 8-bit value, then convert it to a pair value |
pair_to_unsigned(x) | reinterpret a pair value as an unsigned 16-bit value, then convert it to an int value |
Important points:
- Type conversions can cause overflow. For example,
to_byte(257)will produce abytewith value 1. - To convert between classes types, use the
asoperator instead. to_address()produces the memory address of the object being pointed to (not the address of the variable itself). Thus it requires a heap-allocated type such as an array, object, or function pointer.- Although
byte_to_signed()produces apair, you can also assign its result to anint, for examplemy_int <- byte_to_signed(my_byte). This is possible becausepairimplicitly widens toint. - Such widening and narrowing conversions can usually be optimized by the compiler, for example converting
my_bytedirectly intomy_intwithout needing a CPU register for the temporarypairvalue.