Skip to main content

Expressions

Operators, calls, literals, indexing, function values, and the error and concurrency operators.

include "math";

object Point { float x; float y; }
fn roll_damage(int base, bool crit) -> int { if (crit) { return base * 2; } return base; }

Point p = Point { x: 3.0, y: 4.0 };
float dist = sqrt(p.x * p.x + p.y * p.y);
bool in_range = dist < 10.0 && p.x > 0.0;
array<int> wave = [3, 5, 8];
int dmg = roll_damage(base: 10, crit: true)?;
task<int> later = branch roll_damage(base: 4, crit: false);

Precedence​

From tightest to loosest binding:

LevelOperators
1call f(...), member ., index [i], slice [a:b], propagate ?, handle !! { }
2branch, join, !, unary -, unary +
3unless
4* / %
5+ -
6< <= > >=, is
7== !=
8??
9&& (stops early if the left side is false)
10|| (stops early if the left side is true)
11= += -= *= /= %= (right to left: a = b = 4)

Binary operators group left to right. Things to watch for:

  • join t? means join (t?). To pass on a join's failure write (join t)?.
  • join t unless ch? means join t unless (ch?). Write (join t unless ch)?.
  • -f()? means -(f()?).
  • branch f() !! { ... } runs the handler inside the new task.
  • a < b < c is a type error; write a < b && b < c.
  • is does not chain with the other level-6 operators: x is A <= y is a syntax error. x is A == ok means (x is A) == ok.
  • ++, -- and => are statements, not operators; see Statements.

Evaluation order​

  • Operands and call arguments run left to right, in the order written (named arguments too).
  • In an assignment, the value on the right runs before the parts of the target: in a[i()] = v(), v() runs before i().
  • A compound assignment runs the parts of the target first, once: in a[i()] += v(), i() runs, then a[i] is read, then v() runs, then the sum is stored in that same element. See Compound assignment.

Operators​

OperatorOperandsResult
+two numbers, two strings, or a string and a charnumber, or the joined string
- * /two numbersnumber
%two integersint. Not allowed on float; use fmod from math.
unary -, +a numbersame type (unary - on a byte gives int, on a char int32)
< <= > >=two numbers, two strings, or two values of the same enumbool
value is Tan object, interface or engine object value, and such a typebool; see Type tests and downcasts
== !=compatible types (see Equality)bool
!boolbool
&& ||two boolsbool
m ?? ka map and a keybool: whether the key is present. Never fails.
=a writable target and a valuethe assigned value
+= -= *= /= %=a writable target and a valuethe stored value
  • Mixing an int and a float gives a float; see Arithmetic result types.
  • "Score: " + 5 is an error; write "Score: ${5}" (see String interpolation).
  • Integer / rounds toward zero and traps when dividing by zero. Integer overflow wraps around. See Numbers.
  • There are no bitwise, shift or ternary (c ? a : b) operators.

Simple expressions​

FormMeaning
42, 1.5, "text", 'c', true, falseliterals
"text ${expr} text"an interpolated string; see String interpolation
nullno value; only where a nullable type is expected
namea local or parameter; otherwise a top-level constant, variable, or function used as a value
pkg.namea declaration of an included package; see Names and namespaces
E.Membera member of enum E, such as Direction.North
A.Botherwise an engine constant such as Action.ScanSector, or a field access
T(x)a conversion to type T; see Conversions
( expr )grouping
typeof(T)a type value; see Type reflection
switch (v) { p => e, _ => e }the first matching arm's value; see Switch expressions

String interpolation​

string line = "hp ${hp} of ${max}";
string report = "${name} scored ${score * 2} (${ratio})";
  • An interpolated string is a string. Each ${expr} is replaced by the value of expr, and the parts run left to right.

  • An embedded expression may be any expression of these types:

    TypeText
    stringthe string itself
    int, int32, bytedecimal digits, with - when negative
    float, float32the shortest decimal that reads back as the same value
    booltrue or false
    charthe character's UTF-8 bytes; '\0' adds nothing
    enumthe member's name, such as North (never a package prefix)

    Any other type is an error: cannot interpolate a value of type 'T'; convert it to a string first. ${null} is an error too.

  • Floats print in the shortest form that parses back to exactly the same value: 0.1 prints 0.1, 1.0 prints 1, and 0.1 + 0.2 prints 0.30000000000000004. A float32 uses the shortest form for its own precision, so float32(0.1) prints 0.1.

    • Values from 1e-7 up to (but not including) 1e21 print as plain decimals: 1e20 prints 100000000000000000000 and 0.000001 prints 0.000001.
    • Other values use an exponent: 1e+21, 1e-7, 1.5e+300.
    • Special values print as NaN, Infinity and -Infinity, and negative zero prints as -0.
  • A fallible expression follows the usual rules: handle it with ? or !! inside the ${}, for example "hp ${table[id]?}".

Construction​

Object literals​

object Player { string name; Point position; }

Point origin = Point { x: 0.0, y: 0.0 };
Player p = Player { name: "Ada", position: origin, };
  • Works for object types you declare in BT. Engine types and error are created other ways.
  • Fields can be listed in any order, each once (field 'x' is initialized more than once). A trailing comma is allowed.
  • A field you leave out gets 0, 0.0, '\0' or false for numbers, char and bool, its member valued 0 for an enum, and null for nullable types.
  • Fields that have no such default must be given: strings, function values, non-null references including arrays and maps (non-null field 'name' must be initialized), and enums with no member valued 0.
  • Private (~) fields can only be set from the same package.

Array literals​

[1, 2, 3] // array<int>
[1, 2.5] // array<float>
array<float> a = [1, 2]; // holds 1.0 and 2.0
array<array<int>> grid = [[1, 2], [3]];
  • When the literal is assigned, passed or returned to an array<E>, each element is converted to E as an assignment would. Elements that can't be converted are an error (array element does not match the declared element type).
  • Otherwise the element type comes from the elements: all int gives array<int>, and any float among ints gives array<float>.
  • [] is an error (an empty array literal needs an explicit constructor type); write new array<T>().

Map literals​

map<string, int> ports = {"http": 80, "https": 443};
  • The key and value types come from the first entry.
  • {} is an error; write new map<K, V>().
  • A statement cannot start with a map literal, because { starts a block there.

new​

FormResult
new array<T>()empty array
new array<T>(length)length zero-valued elements (if T is an object, array or other reference type, it must be nullable)
new map<K, V>()empty map
new channel<T>(), new channel<T>(capacity)channel; no capacity (or 0) means each send waits for a receiver
new buffer(), new buffer(size)buffer of size zero bytes (needs include "buffer")
new error(code, message)an error
  • A negative length, capacity or size traps.
  • new doesn't work for your objects; use an object literal.

Member access​

value.name reads a field or property, for example player.health, err.message or view.position. Enum.Member names a member of an enum, for example Direction.North; a local variable with the enum's name hides it.

  • Fields of an error (code, message) are read-only.
  • Private (~) fields are visible only in the declaring package.
  • Fields of a component<T>, and some engine fields that can refer to data that has gone away, can fail: use ? or !!, also when assigning ((c.health = 10)?).
  • Accessing a member of null traps. An unknown name is type 'T' has no property 'name'.

Calls​

fn spawn(string kind, int count) { println(kind + " x" + int_to_string(count)); }

spawn("drone", 3)?;
spawn(kind: "drone", count: 3)?;
spawn("drone", count: 3)?;
  • Calls to functions, trees, methods and engine functions accept positional arguments, named arguments, or both. Positional arguments fill parameters from the left. Every parameter needs exactly one argument; there are no defaults.
  • Calls through function values, and built-in array and channel operations, take positional arguments only.
  • Errors: too many arguments to 'f', call to 'f' is missing parameter 'x', parameter 'x' is supplied more than once, function 'f' has no parameter named 'y'.
  • A call to a function that can fail can itself fail, and must be handled: f(g()?)?.
  • value.name(...) on an object calls its method. If there is no such method, it tries an engine extension method for the type, then a field holding a function value. On an interface it calls the actual object's method.
  • To use an engine function, constant or type, the file must include its package: symbol 'sqrt' requires include "math" in this file. See Visibility.

Engine functions that take a type​

Some engine functions work with any type you choose. You pass the type with typeof(...), and the result type follows from it.

include "cturtle/events";

object Ping { int n; }

fn pingStart() -> void {
eventRegister(typeof(Ping), eventFifo())?;
channel<Ping> pings = eventSubscribe(typeof(Ping), 4)?; // gives channel<Ping>
eventFire(Ping { n: 1 })?; // type taken from the value
}
  • Where the API reference shows a parameter of type type<$T>, pass typeof(X) written directly in the call; a type variable is not accepted (F requires a literal typeof(T) for 'p').
  • Where it shows $T, the type is taken from the value you pass.
  • Every use of $T in one call must agree ('F' binds $T to a different type).
  • These functions can't be stored as function values (generic host function 'F' has no single type; call it instead).

Built-in operations​

OperationSignatureCan fail
a.length()-> int (also on slices)no
a.capacity()-> intno
a.reserve(n)(int)no
a.resize(n)(int); new slots are zero values, so a reference element type must be nullableno
a.fill(v)(T); sets every elementno
a.push(v)(T)no
a.pop()-> T; fails on an empty arrayyes
a.insert(i, v)(int, T); i == length() appends; fails on a bad indexyes
a.remove(i)(int) -> T; fails on a bad indexyes
a.clear()removes all elements, keeps capacityno
s.capacity(), s.push(v), s.extend(seq)on a slice: room to grow, append one element, append an array or slice of the same element type; see Slicesno
m.length()-> int: the number of keysno
m.keys(), m.values()-> array<K>, -> array<V>: new arrays in loop order; see Mapsno
c.send(v)(T); waits for space or a receiver; fails if closedyes
c.receive()-> T; waits for a value; fails once closed and emptyyes
c.close()safe to call twiceno
sqrt(x)(float) -> float; needs include "math"no
  • Each array operation also has a function form: array_length(a), array_push(a, v), and so on. Channel operations have send(c, v), receive(c) and close(c).
  • A negative reserve or resize traps.
  • Failure codes: pop, insert and remove fail with -102; send and receive on a closed channel with -300.

Indexing​

ExpressionOnRule
a[i]array, sliceOut of range traps. Can be assigned.
m[k]map (reading)Fails with code 1 if the key is missing, except inside if (m ?? k) (see if).
m[k] = vmap (writing)Inserts or overwrites. Never fails.
r[i]rows<Q, A>Fails if out of range. Gives a view<Q, A>.

Strings and buffers can't be indexed with [].

Slicing​

values[1:4] values[:2] values[3:] values[:]
  • Works on arrays and slices (giving a slice<T>) and buffers (giving a slice<byte>).
  • The start is included and the end is not. Either can be left out. Bounds outside 0 <= low <= high <= length trap, except that reslicing a slice may reach into its spare capacity (see Slices).
  • Assigning to a range copies: dst[0:4] = src[4:8]; (see the copy statement).

Assignment​

a = b = 4;
if ((c = a - 1) > 0) { println("c is positive"); }
  • You can assign to locals, parameters, writable fields, array, slice and map elements, and slice ranges.
  • Not writable (assignment target is not writable): error fields, fields through a const view, read-only engine fields, read projections of a view, and function names.

Function values​

Top-level functions, trees and engine functions can be stored and passed as values.

infallible fn add(int a, int b) -> int { return a + b; }

infallible fn(int, int) -> int op = add;
int seven = op(3, 4);
task<int> later = branch op(3, 4);
  • Function values can live in locals, fields, arrays and maps; be passed and returned; compared with ==; called; and branched.
  • Methods and built-ins (send, array_push, sqrt, ...) are not values.
  • There are no lambdas or closures. A function value carries no captured data; pass what it needs as parameters or in an object.
  • A call through a fn(...) (fallible) type can fail even if the function stored there is infallible.

Type tests and downcasts​

interface Shape { fn area() -> float; }
object Circle { float r; fn area() -> float { return 3.14 * this.r * this.r; } }

if (shape is Circle) { ... }
Circle c = Circle(shape)?; // fails unless shape is a Circle
  • value is T is true when value is not null and its actual type is T, or implements T when T is an interface.
  • T(value) gives the same value typed as T. It can fail, with code 3 (value is not a T), when the value is null or not a T; handle it with ? or !!. Converting a const value gives a const T.
  • value must be an object, interface or engine object (one that can implement interfaces), and may be nullable. T is an object, interface or such engine type, written without ? or const.
  • A test that can never be true is an error: when neither side is an interface and the types differ (a 'Circle' is never a 'Square').
  • null is T is an error; a nullable value that holds null tests false.

Switch expressions​

int cost = switch (tier) { 1 => 10, 2, 3 => 25, _ => 100 };
Direction left = switch (facing) {
Direction.North => Direction.West,
Direction.West => Direction.South,
Direction.South => Direction.East,
Direction.East => Direction.North,
};
float area = switch (shape) { Circle c => 3.14 * c.r * c.r, _ => 0.0 };

A switch expression picks the value of the first arm whose patterns match. Patterns follow the switch statement rules; each arm's value is one expression, and arms are separated by commas, with an optional comma after the last.

  • It must cover every value: it needs a _ arm unless its arms list every member of an enum, or both true and false (a switch expression needs a '_' arm).
  • Every arm has one type. Where the destination has a type (a variable, an assignment, a parameter or return), each arm converts to it. Otherwise the result has the arm type the other arms convert to (1 and 2.5 give float); arms with no such type are an error (switch arms have different types: 'string' and 'int'). A null arm makes an object, interface or collection result nullable.
  • Only the chosen arm runs. If the value or the chosen arm can fail, the whole expression can fail.
  • switch starts an expression, so it can sit anywhere a value can, including inside another switch arm.
  • At the start of a statement, switch is a switch statement unless the token after its closing brace continues an expression: ., ?, ??, !!, is, or a binary operator other than -. So switch (n) { 1 => a, _ => b }.value = 7; assigns a field of the chosen object.

Error operators​

FormMeaning
expr?On failure, pass the error on to the caller (or to the enclosing !! guard). On success, gives the value.
call() !! { ... }On failure, run the block with err set to the error, then continue. The result value is discarded.
  • ? on something that cannot fail is '?' requires a fallible expression.
  • !! directly after an expression works only on a call or index that can fail ('!!' requires a fallible function call). For anything else, such as join t, guard a block: { int v = (join t)?; } !! { ... };.
  • int x = f() !! {}; is an error because a handled call has no value. Set the variable inside a guarded block instead.

Full rules: Errors.

Concurrency operators​

FormMeaningType
branch call(...)Work out the arguments now, then run the call as a new task.task<R>
branch call(...) !! { ... }Same, with the handler running in the new task.task<void>
branch { ... }Run the block as a new task.task<T> from its return statements
join taskWait for the task and give its result. Can fail.T
join task unless chAs join task, but if the channel ch fires first, cancel the task and fail with -305.T
ch.receive() unless otherAs ch.receive(), but if other fires first, fail with -305 and take nothing.T
  • branch takes a call (including method and function-value calls) or a block ('branch' expects a function call or block). It never waits and never fails.
  • join needs a task<T> ('join' requires a task value).
  • A channel fires when it holds a value or is closed. Firing never takes the value.
  • The left side of unless must be a join or a channel receive() call ('unless' applies to 'join', a channel receive() or a select). The right side must be a non-null channel of any element type ('unless' requires a non-null channel<T>) that cannot fail.
  • A !! block after the channel handles the whole wait: join t unless stop !! { ... };.

Full rules: Concurrency.

See also​