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:
| Level | Operators |
|---|---|
| 1 | call f(...), member ., index [i], slice [a:b], propagate ?, handle !! { } |
| 2 | branch, join, !, unary -, unary + |
| 3 | unless |
| 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?meansjoin (t?). To pass on a join's failure write(join t)?.join t unless ch?meansjoin t unless (ch?). Write(join t unless ch)?.-f()?means-(f()?).branch f() !! { ... }runs the handler inside the new task.a < b < cis a type error; writea < b && b < c.isdoes not chain with the other level-6 operators:x is A <= yis a syntax error.x is A == okmeans(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 beforei(). - A compound assignment runs the parts of the target first, once: in
a[i()] += v(),i()runs, thena[i]is read, thenv()runs, then the sum is stored in that same element. See Compound assignment.
Operators
| Operator | Operands | Result |
|---|---|---|
+ | two numbers, two strings, or a string and a char | number, or the joined string |
- * / | two numbers | number |
% | two integers | int. Not allowed on float; use fmod from math. |
unary -, + | a number | same type (unary - on a byte gives int, on a char int32) |
< <= > >= | two numbers, two strings, or two values of the same enum | bool |
value is T | an object, interface or engine object value, and such a type | bool; see Type tests and downcasts |
== != | compatible types (see Equality) | bool |
! | bool | bool |
&& || | two bools | bool |
m ?? k | a map and a key | bool: whether the key is present. Never fails. |
= | a writable target and a value | the assigned value |
+= -= *= /= %= | a writable target and a value | the stored value |
- Mixing an
intand afloatgives afloat; see Arithmetic result types. "Score: " + 5is 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
| Form | Meaning |
|---|---|
42, 1.5, "text", 'c', true, false | literals |
"text ${expr} text" | an interpolated string; see String interpolation |
null | no value; only where a nullable type is expected |
name | a local or parameter; otherwise a top-level constant, variable, or function used as a value |
pkg.name | a declaration of an included package; see Names and namespaces |
E.Member | a member of enum E, such as Direction.North |
A.B | otherwise 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 ofexpr, and the parts run left to right. -
An embedded expression may be any expression of these types:
Type Text stringthe string itself int,int32,bytedecimal digits, with -when negativefloat,float32the shortest decimal that reads back as the same value booltrueorfalsecharthe character's UTF-8 bytes; '\0'adds nothingenum the 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.1prints0.1,1.0prints1, and0.1 + 0.2prints0.30000000000000004. Afloat32uses the shortest form for its own precision, sofloat32(0.1)prints0.1.- Values from
1e-7up to (but not including)1e21print as plain decimals:1e20prints100000000000000000000and0.000001prints0.000001. - Other values use an exponent:
1e+21,1e-7,1.5e+300. - Special values print as
NaN,Infinityand-Infinity, and negative zero prints as-0.
- Values from
-
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
errorare 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'orfalsefor numbers,charandbool, its member valued0for an enum, andnullfor 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 valued0. - 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 toEas 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
intgivesarray<int>, and anyfloatamongints givesarray<float>. []is an error (an empty array literal needs an explicit constructor type); writenew 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; writenew map<K, V>().- A statement cannot start with a map literal, because
{starts a block there.
new
| Form | Result |
|---|---|
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.
newdoesn'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
nulltraps. An unknown name istype '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>, passtypeof(X)written directly in the call; atypevariable 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
$Tin 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
| Operation | Signature | Can fail |
|---|---|---|
a.length() | -> int (also on slices) | no |
a.capacity() | -> int | no |
a.reserve(n) | (int) | no |
a.resize(n) | (int); new slots are zero values, so a reference element type must be nullable | no |
a.fill(v) | (T); sets every element | no |
a.push(v) | (T) | no |
a.pop() | -> T; fails on an empty array | yes |
a.insert(i, v) | (int, T); i == length() appends; fails on a bad index | yes |
a.remove(i) | (int) -> T; fails on a bad index | yes |
a.clear() | removes all elements, keeps capacity | no |
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 Slices | no |
m.length() | -> int: the number of keys | no |
m.keys(), m.values() | -> array<K>, -> array<V>: new arrays in loop order; see Maps | no |
c.send(v) | (T); waits for space or a receiver; fails if closed | yes |
c.receive() | -> T; waits for a value; fails once closed and empty | yes |
c.close() | safe to call twice | no |
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 havesend(c, v),receive(c)andclose(c). - A negative
reserveorresizetraps. - Failure codes:
pop,insertandremovefail with-102;sendandreceiveon a closed channel with-300.
Indexing
| Expression | On | Rule |
|---|---|---|
a[i] | array, slice | Out 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] = v | map (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 aslice<byte>). - The start is included and the end is not. Either can be left out. Bounds
outside
0 <= low <= high <= lengthtrap, 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):errorfields, fields through aconstview, read-only engine fields,readprojections 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 isinfallible.
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 Tistruewhenvalueis notnulland its actual type isT, or implementsTwhenTis an interface.T(value)gives the same value typed asT. It can fail, with code3(value is not a T), when the value isnullor not aT; handle it with?or!!. Converting aconstvalue gives aconst T.valuemust be an object, interface or engine object (one that can implement interfaces), and may be nullable.Tis an object, interface or such engine type, written without?orconst.- 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 Tis an error; a nullable value that holdsnulltestsfalse.
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 bothtrueandfalse(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 (1and2.5givefloat); arms with no such type are an error (switch arms have different types: 'string' and 'int'). Anullarm 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.
switchstarts an expression, so it can sit anywhere a value can, including inside another switch arm.- At the start of a statement,
switchis a switch statement unless the token after its closing brace continues an expression:.,?,??,!!,is, or a binary operator other than-. Soswitch (n) { 1 => a, _ => b }.value = 7;assigns a field of the chosen object.
Error operators
| Form | Meaning |
|---|---|
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 asjoin 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
| Form | Meaning | Type |
|---|---|---|
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 task | Wait for the task and give its result. Can fail. | T |
join task unless ch | As join task, but if the channel ch fires first, cancel the task and fail with -305. | T |
ch.receive() unless other | As ch.receive(), but if other fires first, fail with -305 and take nothing. | T |
branchtakes a call (including method and function-value calls) or a block ('branch' expects a function call or block). It never waits and never fails.joinneeds atask<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
unlessmust be ajoinor a channelreceive()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
- Guide: Syntax reference