Skip to main content

operators

The arithmetic, concatenation, comparison, equality, logical, bitwise, and shift operators. Every binary operator requires its two operands to have the same type.

const sum: int32 = (42: int32) + 8;

sum                     // => 50
"hello" ++ " world"     // => "hello world"
(40: int32) < 42        // => true

Precedence​

Binary operators are left-associative and the unary prefixes are right-associative. The levels, from the one that binds loosest to the one that binds tightest:

  • || and |
  • ^
  • && and &
  • == != < <= > >= << >>
  • ++
  • + -
  • * / %
  • unary ! ~ -
  • postfix . [] ()

Some of those placements differ from a C-style layout. ^ sits on its own level between AND and OR. Equality, comparison, and the shifts all share one level. And ++ binds tighter than + and -, so a ++ b + c groups as a ++ (b + c). Ascription binds below every operator, looser than ||.

Arithmetic​

+ - * / % are defined over every numeric type, and the result is the operand type.

  • A non-numeric operand is rejected; ++ is the operator for strings and arrays.
  • Unary - is defined on signed numerics only, so it does not apply to a uintN.
  • Division truncates toward zero, and % takes the sign of its left operand.
  • Division and modulo by zero are runtime failures, so try turns one into none.
(-7: int32) / 2       // => -3
(-7: int32) % 2       // => -1
try((7: int32) / 0)   // => none

Concatenation​

++ joins two strings, or two arrays of one element type, and yields that same type. Mixing a string with a non-string, or arrays of different element types, is rejected.

Comparison​

< <= > >= are defined over the numeric types and yield bit.

  • Comparison is total: it always yields a bit and never fails.
  • A NaN operand is unordered, so all of them yield false when either side is NaN.
  • One consequence is worth stating rather than tripping over: !(a < b) is not a >= b when either operand may be NaN, because both are then false.
sqrt((-1.0: float64)) < (1.0: float64)     // => false
sqrt((-1.0: float64)) >= (1.0: float64)    // => false

Equality​

== and != are defined over any one type shared by both operands, and yield bit. x != y is !(x == y).

  • Equality is structural: primitives by value, tuples and arrays element-wise, structs field-wise with field order irrelevant, variants by constructor and payload.
  • Arrays must match in length as well as contents.
  • NaN == NaN is false, following IEEE-754.
const head: uint8[] = [1, 2];
const longer: uint8[] = [1, 2, 3];

(true, "air") == (true, "air")     // => true
head == longer                     // => false

Logical and bitwise​

&& and || are dual. On bit they are short-circuiting logical AND and OR, so the right operand goes unevaluated once the left decides the result; on intN/uintN they are bitwise AND and OR, and both operands are always evaluated.

  • & and | are synonyms for && and ||, at the same precedence and with the same meaning.
  • ^ is XOR — bitwise on integers, logical on bit.
  • Unary ! and its synonym ~ are dual the same way: logical negation on a bit, bitwise complement on an integer, yielding the operand's own type.
  • None of these are defined on the float types.
true && false        // => false
(5: uint8) || 3      // => 7
(5: uint8) ^ 3       // => 6
!(5: uint32)         // => 4294967290

Shifts​

<< and >> are defined over intN/uintN, and the result is the left operand's type. >> is lexed as two adjacent > tokens, which is why Optional<Optional<T>> closes correctly.

  • Both operands are the same integer type.
  • The shift amount is in [0, N) for an N-bit operand. A literal amount at or above the width is rejected at check time; a runtime amount out of range yields zero.
  • << zero-fills the low bits. >> zero-fills on a uintN and sign-extends on an intN.
(0b01000000: uint8) << 1     // => 128
(0b10000000: uint8) >> 1     // => 64
(-8: int64) >> 2             // => -2

Overflow​

Integer arithmetic is fixed-width and wraps: a uintN result reduces modulo 2^N, and an intN result wraps two's-complement. Floats follow IEEE-754, where overflow yields inf and an invalid operation yields NaN.

  • Wrapping happens at the width inference pinned, so the same literals wrap differently under different annotations.
  • A value_cast is not subject to the wrapping rule. It widens losslessly or reports absence.
  • Evaluation never panics on a numeric operation.
const limit: uint8 = 250;
const ceiling: int8 = 127;

limit + 10      // => 4
ceiling + 1     // => -128

Operand types​

The type each operator group accepts, and what it yields:

  • + - * / % — matching numerics, yielding the operand type.
  • ++ — two strings or two arrays of one element type, yielding that type.
  • < <= > >= — matching numerics, yielding bit.
  • == != — any one type, yielding bit.
  • && || & | ^ — both bit or matching integers, yielding the operand type.
  • << >> — matching integers, yielding the left operand's type.
  • unary ! ~ — a bit or an integer, yielding the operand type.
  • unary - — a signed numeric, yielding the operand type.

Errors​

Operands the operator does not accept​

Both operands must have the same type, and each operator accepts only some types — there is no promotion between widths and no coercion between categories.

const mixed: int32 = (42: int32) + (43: int16);         // error: invalid-operand-types
const crossed: bit = (42: int32) == (42.0: float64);    // error: invalid-operand-types
const negated: uint8 = -(5: uint8);                     // error: invalid-operand-types
const flipped: float32 = !(3.14: float32);              // error: invalid-operand-types
const shifted: uint8 = (1: uint8) << 8;                 // error: invalid-operand-types
  • primitives — the types these operators are defined over.
  • value-cast — the conversion between two numeric types.
  • literals — how an operand's width gets pinned.