Operators

An operator is a special symbol that performs an operation on one or two operands and produces a result. For instance, in the expression 7 + 3 the operator is + (addition), the two operands are 7 and 3, and the result is 10.

The behaviors described below are how Frost's built-in types define these operators to work. They may be overloaded, just as they are in the built-in types.

Frost operators come in several categories:

Arithmetic

Frost's arithmetic operators operate on numbers, and always produce at least an Int32 value, even if the types you are operating on are smaller than that. If either of the two operands is Real, the result is Real. If either of the two operands is 64 bits long, the result is 64 bits long. Thus Int8 Int16 = Int32, Int64 Real32 = Real64, and Real32 * Int32 = Real32. Mixing signed and unsigned integers results in a signed type big enough to hold either of its operands, so adding an Int32 and a UInt32 results in an Int64. It is an error to add an Int64 and a UInt64, because there is no signed type big enough to hold all UInt64 values.

Note that Frost's division operator always produces a Real result, even if you are dividing two integers. To perform an integer division you must use the integer divide (//) operator.

It is a safety violation for any of these operations to result in integer overflow. Use the unchecked operators below to perform math which can overflow.

Unchecked Arithmetic

The unchecked arithmetic operators function exactly like the normal arithmetic operators, except that they do not detect integer overflow. The resulting answer is the true answer modulo 2^n, where n is the bit width of the operation.

Range

The range operators provide a shorthand syntax for creating Range and SteppedRange objects. The range operators take an optional start value, an optional end value, and an optional step value, so the following ranges are all valid:

Range and SteppedRange are used in many of Frost's APIs. They are used to specify subranges of List, substrings of String, and as a target of for loops.

Shift

The shift left operator shifts the bits in a number to the left, inserting zero bits on the right. Left shifting by n bits is equivalent to multiplying by 2^n with no overflow checking.

The shift right operator shifts the bits in a number to the right. For unsigned types zero bits are inserted on the left, and for signed types copies of the sign bit are inserted on the left. Right shifting by n bits is equivalent to dividing by 2^n.

Comparison

The comparison operators all follow standard arithmetic rules and produce a Bit result, either true or false. The = operator checks for equality rather than identity; in other words string1 = string2 checks whether the two strings have the same value (the same sequence of characters), not whether they are actually the same string object.

The comparison operators are defined by the Equatable and Comparable interfaces. Generally speaking, if you implement any of these operators you should also implement the Equatable or Comparable interface.

Identity

The identity operator checks whether two objects are in fact the same object. The MutableString() == MutableString() example returns false because two distinct MutableString objects are being created. They are equal, because they contain the same (zero-length) sequence of characters, but they are not identical, because they are two distinct objects. Identity is a seldom-used operation; you will almost always want to compare objects for equality rather than for identity.

The identity / not identity operators are not allowed to operate on value objects, as value objects do not have a well-defined identity. If you "trick" Frost into comparing the identity of two value objects, for instance:

def a:Object := 5
def b:Object := 5
Console.printLine(a == b)

the result is undefined. This may display either true or false, and the output may change with compiler settings, environment, context, or version of Frost.

Logic

The logical operators implement the standard boolean logic functions.

Short-Circuiting

The logical and and logical or operators on Bit values are short-circuiting: that is, they do not evaluate the right-hand operand unless they need to. If the left-hand operand of a logical and is false, then the result of the logical and is false no matter what the right-hand operand evaluates to, and so the right-hand operand is not evaluated. Likewise, if the left-hand operand of a logical or is true, then the result of the logical or is true no matter what the right-hand operand evaluates to, and so the right-hand operand is not evaluated.

This short-circuiting behavior is built into the compiler for Bit values; there is no way to replicate this behavior on user-defined types.

Bitwise

The bitwise operators implement the standard boolean logic functions bit-by-bit on two integers. Unlike the logical operators, the bitwise operators always evaluate both of their operands.

Cast

The cast operator tells the compiler to treat an object as being a different type. For instance, in

def x:Object := "Hello"
processString(x)

assuming processString is declared to take a single parameter of type String, this code will produce the message no match found for method processString(frost.core.Object). As far as the compiler is concerned, the variable x has type Object, and so one cannot call the method processString on it.

Since the value x holds is actually a String, we can inform the compiler of this via the cast operator:

processString(x->String)

This statement casts x to the type String. Casting doesn't actually change the value; it just instructs the compiler to assume that it is a different type. An invalid cast - that is, casting an object whose runtime type turns out to not actually match the target type - is a safety violation.

The force non-null operator (postfix exclamation mark, !) is shorthand for casting a nullable value to its non-nullable equivalent. If nullableString represents a value of type frost.core.String?, the expression nullableString! is exactly equivalent to the expression nullableString->String.

There is one situation in which the force non-null operator may behave in a surprising fashion. If you have a generic type, such as in:

class Example<T> {
    def field:T?
}

then the expression field! casts field from T? to T. But since T is a generic type, it might also be nullable. If that were the case it would mean that it is actually ok for field to be null, despite the presence of the force non-null operator. In this example, if T is nullable the expression field! will never result in a safety violation, even when field is null.

Index

The index operator is used to reference elements in collections, while the indexed assignment operator modifies collection elements. Many built-in classes which define the index operators also define them to work on Range and SteppedRange, so you can for instance reverse the order of a list simply by writing:

list[..] := list[.. by -1]

Operator Precedence

See expressions for a description of operator precedence.