Expressions

Antril's expressions - precedence and associativity, member access and null operators, calls, indexing, new, array literals, tuples, lambdas, typeof, sizeof, nameof, default, interpolated strings, casts and conversions, is, as, switch and with.

Last updated 16 min read Edit on GitHub

On this page

Precedence

From the loosest to the tightest. Operators in the same row have the same precedence; the level number is the one of PrecedenceLevel, which new operator symbols use to place themselves (see Operators).

Level PrecedenceLevel Operators Associativity
10 Assignment = += -= *= /= %= &= |= ^= <<= >>= right
20 Conditional c ? a : b right
30 Coalesce ?? right
40 LogicalOr || left
50 LogicalAnd && left
60 BitwiseOr | left
70 BitwiseXor ^ left
80 BitwiseAnd & left
90 Equality == != left
100 Relational < <= > >=, is, as left
105 x switch { … } left
110 Range .. ..= left
120 Shift << >> left
130 Additive + - left
140 Multiplicative * / % left
141 Power (none built in; for new symbols)
unary prefix - + ! ~ ++ -- & &mut * await, casts (T)x right
postfix calls f(…), ., ?., […], ->, x++ x--, with { … } left

Some consequences:

  • == binds tighter than &, | and ^: write (flags & Mask) == Mask. Without the parentheses, flags & Mask == Mask compares Mask == Mask first.
  • Comparisons do not chain: 1 < 2 < 3 compares a bool with 3 (AN2080).
  • a ?? b + c is a ?? (b + c), and a ?? b ?? c groups to the right.
  • c ? x : d ? y : z is c ? x : (d ? y : z).
  • An assignment is an expression with the assigned value: a = b = 4; sets both.
  • -5 is a negative literal, so -2147483648 fits an i32.
AntrilRun
import System;

Main()
{
	i32? missing = null;
	Console.WriteLine(missing ?? 2 + 3);
	Console.WriteLine(1 + 2 * 3 << 1);
	Console.WriteLine((6 & 3) == 2);
	bool t = false;
	Console.WriteLine(t ? 1 : t ? 2 : 3);

	i32 x = 5;
	x >>= 1;
	x <<= 3;
	x ^= 1;
	x %= 7;
	Console.WriteLine(x);
	i32 y = x++ + ++x;
	Console.WriteLine(y + " " + x);
	Console.WriteLine(-x * 2);
	Console.WriteLine(~0);
	Console.WriteLine(3 is > 2 and < 5);
	Console.WriteLine(7 switch { > 5 => 1, _ => 0 } + 10);
}
Terminal
larva run
5
14
true
3
3
8 5
-10
-1
true
11

Operands

Operand Example See
Literals 42, 0xFF, 1.5f32, 'A', "text", true, null Lexical structure
Interpolated strings $"n = {n}" below
Names count, Console, System.Math below
this, base this.x, base.Describe() Members
Parentheses (a + b)
Tuples (1, "one") below
Array literals [1, 2, 3] below
new new Point(1, 2), new i32[10] below
Lambdas x => x * 2 below
typeof, sizeof, nameof, default typeof(str), sizeof(i64) below
checked, unchecked checked(a + b) below
effect { … } Effect e = effect { … }; Reactive values
$resource("name") $resource("data/table.bin") below
declbits, allbits in a bitflag enum Enums

Names and member access

A name is looked up in the current function (locals and parameters), then the members of the type (and its base classes), then the namespace and its parents, then the names imported by name, and last the namespaces imported whole (see Source files). A name can always be written in full: System.Console.WriteLine.

a.b reads a member: a field, a property, a method, a nested type, or a member of a namespace. Static members are read through the type (Int32.MaxValue); reading a static member through a value is AN2038, and an instance member through a type is AN2036. A primitive keyword names its type: i32.MaxValue.

Null operators

Operator Value
a?.b, a?.M() null when a is null; otherwise a.b (as a nullable).
a ?? b a unless it is null, then b.

A member of a nullable value cannot be read directly: n.Value with Node? n is AN2040 (check it first, with is, ?. or ??).

AntrilRun
import System;

public class Node
{
	public:
		i32 Value;
		Node? Next;
		new(i32 Value);
		Describe() : str => "node " + Value;
}

Main()
{
	Node first = new Node(1);
	first.Next = new Node(2);
	Console.WriteLine(first.Next?.Value ?? -1);
	Console.WriteLine(first.Next?.Next?.Value ?? -1);
	Console.WriteLine(first.Next?.Describe() ?? "none");
	i32? v = first.Next?.Next?.Value;
	Console.WriteLine(v.HasValue);
	i32? a = null, b = null;
	Console.WriteLine(a ?? b ?? 9);
}
Terminal
larva run
2
-1
node 2
false
9

?? needs a left side that may be null (AN2079), and comparing a value that is never null with null is AN2075.

Calls

f(a, b) calls a function, a method or a function value. The overload is chosen by the number of arguments, then by their types (see Functions). Type arguments are usually inferred; they can be written after the name, before the parentheses:

Antril
Make<Box>();
Pair<i32, str>(1, "x");

< after a name is read as type arguments only when a ( or . follows the closing >; otherwise it is a comparison.

Arguments are passed by position. A value argument is copied; an object argument passes the reference.

Warning

Not yet available: default values for parameters and named arguments (F(b: 2)). Write overloads instead.

Indexing

Target x[i] x[i] = v
An array T[] the element; the index is any integer stores the element
A multi-dimensional array T[,] g[i, j]: one index per dimension (AN2043) stores
A pointer T* (unsafe) *(p + i) stores through it
A fixed buffer the element stores
Any other object or value calls x.At(i) calls x.SetAt(i, v)

Any number of indexes can be given to At and SetAt: grid[x, y] calls grid.At(x, y). This is how List<T>, Dictionary<K, V> and str are indexed, and how your own types become indexable:

AntrilRun
import System;

public class Board
{
	i32[] cells = new i32[9];

	public:
		At(i32 x, i32 y) : i32 => cells[y * 3 + x];
		SetAt(i32 x, i32 y, i32 value) { cells[y * 3 + x] = value; }
}

Main()
{
	Board b = new Board();
	b[1, 2] = 7;
	b[1, 2] = b[1, 2] + 1;
	Console.WriteLine(b[1, 2]);

	Dictionary<str, i32> ages = new Dictionary<str, i32>();
	ages["ana"] = 31;
	Console.WriteLine(ages["ana"]);
}
Terminal
larva run
8
31

Array indexes are checked: an index out of range stops the program with larva panic: index 3 out of bounds for length 1 and exit code 101. A type without At cannot be indexed (AN2042).

Warning

Not yet available: compound assignment on an indexed object, such as list[0] += 1 (AN2096). Write list[0] = list[0] + 1. On arrays it works.

new

Form What it makes
new C(args) An object of the class C, by the constructor that takes args.
new V(args), new V() A value: built in place, not on the heap.
new T[n] An array of n elements, zeroed.
new T?[n] An array of n nullable elements, all null.
new T[a, b] A rectangular array (up to 8 dimensions).
new T() With T : new() in a generic; see Generics.

The arguments can be left out for a constructor without parameters: new Thing is new Thing(). A base class (AN2061), a static class (AN2060) and a type without a matching constructor (AN2062) cannot be made.

A new array starts zeroed, so its elements need a zero value: new str[3] is AN2022. Use new str?[3], an array literal, or a List<str>.

Warning

Not yet available: array fillers, such as new str[3]("").

Array literals

[a, b, c] makes an array. Its element type comes from where it goes (a variable, a parameter, a return), or else from its elements:

AntrilRun
import System;

Main()
{
	i64[] wide = [1, 2];
	i32[] none = [];
	i32[][] rows = [[1], [2, 3]];
	i32[,] square = [[1, 2], [3, 4]];
	Console.WriteLine(wide[1] + none.Length + rows[1].Length);
	Console.WriteLine(square[1, 0] + " " + square.Rank + " " + square.GetLength(1));
	loop (str word in ["alpha", "beta"])
		Console.Write(word + " ");
	Console.WriteLine();
}
Terminal
larva run
4
3 2 2
alpha beta 
  • An empty literal [] needs a target type (AN2024).
  • Nested literals make an array of arrays (i32[][]), or a rectangular array when the target is T[,]; then every row has the same length.

Tuples

(a, b, …) with two or more items makes a System.ValueTuple, whose type is written (T1, T2, …). Its items are Item1, Item2… A null item needs a target type: (str?, i32) pair = (null, 1); (AN2143 otherwise). See Types.

Ranges

a..b (without b) and a..=b (with b) are not values: they go in a range loop (loop (i32 i = 0..10)) and in patterns (1..10 =>). Anywhere else they are AN2026.

Lambdas

A lambda is a function written in place: x => x * 2, (a, b) => a + b, (str s) => { … }, () => 42. Its parameter types come from the func type it goes to, or are written. See Functions.

Antril
func<i32, i32> : i32 add = (a, b) => a + b;
func<str> show = (str s) => { Console.WriteLine("> " + s); };

typeof, sizeof, nameof and default

Expression Value
typeof(T) The System.Type that describes T: typeof(List<i32>).Name is "List".
sizeof(T) The size of T in bytes, as an i32: sizeof(u256) is 32. A constant for types whose size is known.
nameof(x) The last name written, as a constant str: nameof(count) is "count", nameof(Console.WriteLine) is "WriteLine". For a type, its name as typeof(T).Name gives it: nameof(i32[]) is "Int32[]".
default(T) The zero value of T: 0, false, a zeroed value, null for a nullable type.

nameof of a name that does not exist is AN2141. Inside a generic, nameof(T) is the name of the type argument, read at run time.

Caution

default(str) and default(C) for a class compile, but give a reference that is not valid: using it crashes the program. Use default(str?), which is null.

checked and unchecked

checked(expression) evaluates integer operations with overflow checks: a result that does not fit throws OverflowError. unchecked(expression) wraps around, which is also what happens by default. The statement forms are in Statements.

Interpolated strings

$"…" builds a string from text and holes {expression}. A hole can have an alignment after , and a format after ::

Hole Meaning
{value} The value as text, as + would join it.
{value,8} Padded with spaces on the left to 8 characters (right-aligned).
{value,-8} Padded on the right (left-aligned). The width can be any integer expression.
{value:F2} value.ToString("F2"): the format goes to the value's ToString(format).
{value,8:F1} Both.

{{ and }} write a brace. The format runs to the closing }.

AntrilRun
import System;

Main()
{
	f64 price = 3.14159;
	i32 n = 42;
	str name = "Ana";
	Console.WriteLine($"{name} has {n} items at {price:F2}");
	Console.WriteLine($"[{n,6}] [{n,-6}] [{price,8:F1}]");
	Console.WriteLine($"{{braces}} {n * 2} {(n > 40 ? 1 : 0)} {n:X}");
	Console.WriteLine($"{name.ToUpper()} {$"[{n}]"}");
}
Terminal
larva run
Ana has 42 items at 3.14
[    42] [42    ] [     3.1]
{braces} 84 1 2A
ANA [42]

A ?: in a hole goes in parentheses, since : starts the format. An interpolated string is one line.

Warning

Not yet available: a string literal inside a hole, such as $"{ok ? "yes" : "no"}" (AN0007). Put the text in a variable first. An interpolated string inside a hole works.

Strings and +

+ with a string on either side joins text: the other side is turned into text with its ToString(). Numbers, bools, enums and objects are joined; evaluation goes left to right, so 1 + 2 + "x" is "3x" and "x" + 1 + 2 is "x12".

A nullable string (str?) and a type parameter without a constraint cannot be joined (AN2097): check the string for null first (name ?? "").

Casts and conversions

Conversions that cannot lose information happen by themselves:

From To
A signed integer A wider signed integer.
An unsigned integer A wider unsigned integer, or a wider signed one (u16 to i32).
An integer Any float.
A float A wider float (f32 to f64).
T T?.
A class Its base classes and its interfaces, and any.
A value, a number An interface it implements, and any (boxed).
&mut T &T.

Every other conversion is a cast, (T)x, and without it the compiler reports AN2084 (a i64 is not a i32 (cast it: it may lose information)):

Cast What it does
(i32)wide An integer to a narrower or differently signed one keeps the low bits: (i8)200 is -56, (u8)-1 is 255.
(i32)3.9 A float to an integer drops the fraction: -3.9 gives -3.
(f32)x Between floats, rounded.
(i32)level, (Level)5 An enum to and from its base; see Enums.
(Dog)animal A downcast: the object must be a Dog.
(i32)boxed From any (or an interface) back to the value it holds.
(T)x A conversion a type declares with op (T)(S s); see Operators.
(u8*)p, (i64)p Between pointers, and between pointers and integers, in unsafe code (AN2083).

A downcast gives a non-null object, so its target is T, not T? (AN2082). When the object is not of that class the program stops (larva panic: null reference, exit code 101); test first with is, or use as.

x as T converts an object to the class or interface T and gives a T?: null when x is not a T. It works between object types only (AN2020).

is

x is pattern tests a value against a pattern and gives a bool. A type pattern with a name declares a variable that is set where the test is true:

Antril
if (shape is Circle c && c.Radius > 1)
	Console.WriteLine(c.Radius);
if (name is not null)
	Console.WriteLine(name.Length);
if (point is { X: 0, Y: > 1 })
	Console.WriteLine("on the axis");

Every pattern is described in Patterns.

switch expressions

value switch { pattern => result, … } gives the result of the first arm that matches. Arms are separated by commas (a last comma is allowed), and each can have a when condition:

AntrilRun
import System;

Describe(i32 n) : str => n switch
{
	< 0 => "negative",
	0 => "zero",
	1..10 => "small",
	_ => "large",
};

Main()
{
	Console.WriteLine(Describe(-3) + ", " + Describe(0) + ", " + Describe(5) + ", " + Describe(50));
}
Terminal
larva run
negative, zero, small, large

The type of the expression is the type the arms have in common. A switch expression needs at least one arm (AN2085). The compiler does not check that the arms cover every value: when none matches, the program stops with larva panic: null reference (exit code 101). End with _ => ….

with

record with { Field = value, … } copies a record class or record value, replacing some fields:

AntrilRun
import System;

public record class Person
{
	public:
		str Name;
		i32 Age;
		new(str Name, i32 Age);
}

Main()
{
	Person ana = new Person("Ana", 31);
	Person older = ana with { Age = 32 };
	Console.WriteLine(ana);
	Console.WriteLine(older);
}
Terminal
larva run
Person { Name = Ana, Age = 31 }
Person { Name = Ana, Age = 32 }

with on a type that is not a record is AN2132, and a field the record does not have is AN2133.

Pointer operators

In unsafe code: &x takes an address, *p reads or writes through a pointer, p->field is (*p).field, p[i] is *(p + i), and p + n, p - q and comparisons do pointer arithmetic. Outside unsafe code they are AN2066, AN2067 and AN2121. With the borrow checker on, &x and &mut x in safe code borrow instead; see Memory.

Resources

$resource("name") is the bytes of a file built into the program, as a u8[]. The name is one listed in the RESOURCES chunk of the project's .queen; a name that is not there is AN2139.

.queen
chunk RESOURCES:
   embed: data/**
Antril
u8[] table = $resource("data/table.bin");

System.Resources reads them by a name known only at run time.

Was this page helpful?

No address is stored; your user name is, if you are signed in.

Esc