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.
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
|
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()
.
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
andfalse
are the truth values of thebool
type. -
null
is a special value of any reference type that supports it, i.e. either a nullable type or a type decorated with theAllowNullLiteral
attribute. Unlike in C#,Nullable<>
does not acceptnull
as a value. -
unit
is the single value of theunit
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 supportnull
, 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 operators express logical connectives, used to manipulate values of the bool
type.
-
and
is the logical conjunction ‒ it istrue
if all operands aretrue
. -
or
is the logical disjunction ‒ it istrue
if any operand istrue
. -
not
is the logical negation ‒ it changestrue
tofalse
andfalse
totrue
. -
if
…elseif
…else
is the conditional operator ‒ it evaluates to the value corresponding to the firsttrue
condition, or to the one afterelse
(which is mandatory).
and
, or
, and if
…elseif
…else
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
ornot(a and b)
. -
a and b or c
is likewise disallowed, since it may be interpreted as either(a and b) or c
ora and (b or c)
. -
if … else a and b
is also prohibited, due to(if … else a) and b
orif … 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 asnot
is in the last operand only). -
not not a
is also fine, as well asnot if …
.
To work around these restrictions, parentheses or boolean operators must be used, to indicate the correct precedence.
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 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.
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:
- Any reference type, checking for
null
. -
Nullable<>
. -
ValueOption<>
(the default implementation of?
). -
Option<>
. -
Result<,>
.
The right operand may also be an option-like type, in which case the result is promoted to that type.
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
orlist
, 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).
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, callsf
withx
as the first argument. -
|
is used for piping:x | f
is equivalent tof(x)
. -
<<
and>>
combine functions together:(f >> g)(x)
is equivalent tog(f(x))
and(f << g)(x)
is equivalent tof(g(x))
.
-
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
.
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.
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 ‒ useEnum.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 typeT
to typeT?
, 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 toobject
), equivalent to callingbox
. -
void
evaluates the expression but results inunit
, equivalent to callingignore
. - 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
orsbyte
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
, orOverflowException
),none
is returned instead. For example,int? x
attempts a conversion ofx
toint
and results in a value of typeint?
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 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<…>
, thuslet list as List<> = new(capacity: 10)
is equivalent tolet list = new(capacity: 10) as List<>
orlet 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.