Skip to main content

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

ExamplePurpose
123
$0000_ff23
integer literal
"hello{n}"string literal
'{n}'character literal
@my_labeldata label
nullnull object
selfobject 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 groupExamplePurpose
__ . __
__ :: __
__ ( __ )
__ [ __ ]
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 + -5negate a number
__ * __
__ / __
__ % __
__ // __
__ %% __
x * y / 2multiply, divide, remainder
__ + __
__ - __
2 + 2add and subtract numbers
__ as __target as playertype cast for class objects
__ <= __
__ < __
__ = __
__ > __
__ >= __
__ <> __
x <> 3compare two numbers
not __not x > 3boolean NOT
__ and __x < 0 and x > 3boolean AND
__ or __x < 0 or x > 3boolean 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 quotientTruncated remainder
17 / 5 = 317 % 5 = 2
-17 / 5 = -3-17 % 5 = -2
17 / -5 = -317 % -5 = 2
-17 / -5 = 3-17 % -5 = -2
Euclidean quotientEuclidean remainder
17 // 5 = 317 %% 5 = 2
-17 // 5 = -4-17 %% 5 = 3
17 // -5 = -317 %% -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.

ExamplePurpose
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 a byte with value 1.
  • To convert between classes types, use the as operator 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 a pair, you can also assign its result to an int, for example my_int <- byte_to_signed(my_byte). This is possible because pair implicitly widens to int.
  • Such widening and narrowing conversions can usually be optimized by the compiler, for example converting my_byte directly into my_int without needing a CPU register for the temporary pair value.