Symbols and operators - IS4Code/Sona GitHub Wiki

This page provides a reference for all operators and other special pieces of syntax provided by the language.

Operators

The following table lists all existing operators, from the lowest precedence to the highest.

Category Operators Associativity
Logical and or not if … elseif … else Mutually nonassociative*
Boolean || Left
&&
Relational ==, !=, ~=, <, <=, >, >= Nonassociative**
Coalescing ?? Right
Range/concatenation .. Left
Bitwise/functional | Left
^ Right
& Left
<<, >>
Arithmetic +, - Left
*, /, %
Postfix as, with Left
Conversion/construction new, some, widen, narrow, follow, enum,
unit, implicit, explicit, type conversions
Not applicable***
Unary +, -, ~, !, #
Member access ? Right****
., :, (), [] Left
Conversion/construction Same as above, but followed by parentheses.***** Not applicable***
Nullary true, false, null, unit, none, default
* Logical operators cannot be mixed in situations where the precedence could affect the result, e.g. a and b or c is not allowed. For this reason not a and b is prohibited but a and not b is not. Individual logical operators themselves are left-associative.
** Multiple relational operators in a row (such as a > b > c) are prohibited.
*** Nullary and unary operators do not have well-defined associativity.
**** The conditional member access operator (?) must be followed by a normal member access operator and causes it and all other following member access to be conditionally evaluated.
***** When followed by parentheses, these operators mimic function calls, for example int(x).ToString() is parsed as (int(x)).ToString().

Constants

Constant values can be also thought of as nullary operators, i.e. not taking any operands. They result in identical values when used repeatedly, but their type can differ based on context.

  • true and false are the truth values of the bool type.
  • null is a special value of any reference type that supports it, i.e. either a nullable type or a type decorated with the AllowNullLiteral attribute. Unlike in C#, Nullable<> does not accept null as a value.
  • unit is the single value of the unit type.
  • none is the default value of the option type, ?.
  • default provides the default value of its inferred type. It is allowed only for types that support null, option types, Nullable<>, unit, and all value types that do not contain references (primitive types, enums, etc.).

Most of these constants are also usable in a type-directed manner, such as default as Nullable<> or none as int? to specify the type of the result, partially or fully.

Logical

Logical operators express logical connectives, used to manipulate values of the bool type.

  • and is the logical conjunction ‒ it is true if all operands are true.
  • or is the logical disjunction ‒ it is true if any operand is true.
  • not is the logical negation ‒ it changes true to false and false to true.
  • ifelseifelse is the conditional operator ‒ it evaluates to the value corresponding to the first true condition, or to the one after else (which is mandatory).

and, or, and ifelseifelse perform short-circuit evaluation ‒ the moment the result is known, it is used without evaluating the remaining conditions.

These operators have mutually incomparable precedence, so they may be used in parallel only if the precedence cannot affect the interpretation, for example:

  • not a and b is ambiguous and therefore not allowed, since it could be read as either (not a) and b or not(a and b).
  • a and b or c is likewise disallowed, since it may be interpreted as either (a and b) or c or a and (b or c).
  • if … else a and b is also prohibited, due to (if … else a) and b or if … else (a and b).

Some parallel usage of these operators is allowed:

  • a and not b is okay since there is no ambiguity (as long as not is in the last operand only).
  • not not a is also fine, as well as not if ….

To work around these restrictions, parentheses or boolean operators must be used, to indicate the correct precedence.

Boolean

Boolean operators && and || have the exact same semantics as the logical operators and and or, but with deterministic precedence ‒ && has higher priority than ||, meaning an expression like a || b && c is well-defined to be equivalent to a || (b && c). This is the usual behaviour in other languages, based on the interpretation of || as addition and && as multiplication.

Relational

Relational operators express relations between values, resulting in a bool value whether the relation holds.

  • == compares the two operands for equality.
  • != or ~= compares the two operands for inequality.
  • < and > check whether one operand is less/greater than the other.
  • <= and >= check whether one operand is less/greater than or equal to the other.

These operators cannot be used in a sequence, for example a == b == c is prohibited. This syntax is reserved due to the potential to be interpreted as "a == b and b == c", as is used in mathematics and some other languages.

Coalescing

The coalescing operator ?? evaluates the left operand as an option-like type and extracts its value. If the value is not present, it instead evaluates the right operand and uses its value as the result. The type of the result is the same as the type of the right operand.

Supported option-like types are:

The right operand may also be an option-like type, in which case the result is promoted to that type.

Range/concatenation

The .. operator constructs a sequence type (.. and else) from its operands. The operation depends on the types of the operands:

  • If the operands are of a numeric type, the result is a range from the left operand to the right (by 1), inclusive.
  • If the operands are both string or list, the result is the concatenation of them, as a new object of the same type.
  • If the operands are sequences (..), the result is the concatenation of those sequences, lazily evaluated (as ..).

.. is also used as a pseudo-operator in constructions of tuples, sequences, and collections, as well as function calls, where it serves as the spread operator (extracts individual elements from a container instead of using it as a single element).

Bitwise/functional

The operators &, |, ^, <<, and >> are overloaded, depending on the types of their arguments:

  • For integral types, they correspond to the usual bitwise operations (AND, OR, XOT, left shift, right shift).
  • For floating-point types, ^ denotes power/exponentiation.
  • When one of the operands is a function, the result depends on the operation:
    • & is used for partial function application: f & x produces a new function that, when called, calls f with x as the first argument.
    • | is used for piping: x | f is equivalent to f(x).
    • << and >> combine functions together: (f >> g)(x) is equivalent to g(f(x)) and (f << g)(x) is equivalent to f(g(x)).

as

The as operator does not perform any specific operation, but it asserts the type of its operand, which may be helpful in type-directing other expressions. let v = x as T is equivalent to let v as T = x.

with

The with operator constructs a new record instance by copying existing record fields and updating them with new values. The right operator must always be a record constructor.

Conversions

Conversion operators transform one value into another using one of the several built-in conversions:

  • widen casts an object to a less derived type (base class or implemented interface). The compiler ensures this conversion is valid.
  • narrow casts an object to a more derived type. This conversion may fail at run-time if the object is not compatible with the type.
  • enum converts between an enum value and its underlying type. If used with a string argument, it also attempts to parse it to the enum type. This conversion does not check if the value is actually defined by the enum type ‒ use Enum.IsDefined instead.
  • unit changes the unit of measurement of the value, without affecting the numeric value itself.
  • implicit invokes the implicit conversion operator to perform the conversion. This operator should generally not fail.
  • explicit invokes the explicit conversion operator. This operation may result in a user-generated exception.
  • new invokes the constructor on the target type, using the value as its sole argument.
  • some transforms a value of type T to type T?, creating an instance of an option type that holds the value.
  • follow extracts the raw value from a monadic type by performing the corresponding "bind" operation on it. It is a pseudo-operator, only usable in some contexts.

These operators may also be decorated with a generic argument inside <…>, to aid in type-directing the operation. For example, some<int>(x), some x as int?, and some(x as int) are all equivalent. The argument generally expresses the type of the result, with the exception of some (contains only element type the option is constructed from) and unit (contains only the unit, since the underlying type is the same).

A named type may also be used in this context, to attempt a conversion to that type, with these special cases:

  • object boxes the value (casts it to object), equivalent to calling box.
  • void evaluates the expression but results in unit, equivalent to calling ignore.
  • A numeric type with an operand that is a numeric literal results in a literal of that type. This works for all number literals (including hexadecimal and exponential notation) as long as the type can express the literal value.
  • byte or sbyte used with a character literal result in an ASCII character literal.

The conversion operator may also be optionally followed by ?, returning an option type instead. This has the following effects:

  • If the conversion would result in an exception on failure (ArgumentException, InvalidCastException, FormatException, or OverflowException), none is returned instead. For example, int? x attempts a conversion of x to int and results in a value of type int? indicating whether the conversion succeeded.
  • The operand may be an option-like type, in which case the operation is performed on its value, if present.

When the operand is in parentheses, any following member access pertains to the result of the operation, not to the operand. For example, int(x).ToString() obtains the string value of the result of applying int, not the integer value of x.ToString().

Constructions

Constructions are usable in the same place as conversions and have similar syntax, but they are used to construct instances of types and as such may take arbitrary arguments like in method calls, unlike conversions which accept only a single operand.

  • new constructs a new instance of an arbitrary type from arguments passed to its constructor. The type is inferred from context, or may be specified within <…>, thus let list as List<> = new(capacity: 10) is equivalent to let list = new(capacity: 10) as List<> or let list = new<List<>>(capacity: 10).

Using a named type is also possible to achieve its construction, but this only happens when the constructor call is clearly indicated, i.e. via parentheses and as long as at least one of these conditions is met:

  • The parentheses are empty (e.g. int()).
  • There are more than one argument (string('a', 3)).
  • Field assignment is used (string(value = ['x', 'y'])).
  • The spread pseudo-operator is used.
⚠️ **GitHub.com Fallback** ⚠️