Syntax Reference
A compact cheat sheet for BT source syntax. Earlier chapters explain the rules with examples. The complete rules, with every error message and limit, are in the BT language reference.
Source file
A source file contains declarations. Includes conventionally come first:
include "module/package";
include "module/other" as o; // o.name names that package's declarations
object Name { ... }
interface Name { ... }
enum Name { First, Second = 4, Third }
query Name { Component as alias; ... }
access Profile for Name { read alias; write otherAlias; ... }
infallible fn name(...) -> Type { ... }
fn name(...) -> Type { ... }
tree name(...) -> Type { ... }
const Type NAME = constant_expression;
Type name = expression; // a top-level variable
An include names a package by module path, never a file or relative path.
Files in the same folder are one package and need no includes between them.
Declarations may be used before they appear. A leading ~ (~fn helper(...),
~object Name { ... }) makes a declaration private to its package. Each
package has its own names; package.name (the include's last path segment, or
its as alias) picks one. See
Functions and modules.
Identifiers
An identifier begins with a letter, underscore, or non-ASCII UTF-8 byte and continues with those or digits. Names are case-sensitive. Identifiers are not Unicode-normalized, so keep one normalization form in a project.
Reserved words:
include object interface fn infallible const tree
if else while repeat for in select
break continue return throw
branch join discard new typeof unless
true false null
enum switch is
NaN and Infinity are float literals, so they cannot be names either.
select is still accepted as a member or method name (tabs.select(0)).
try and catch are ordinary identifiers. query, access, read, and
write are keywords only inside schema declarations, and as only there and
after an include path.
Types
Built-in type names are lowercase. Error, Task, or Array name different,
normally unknown, types.
void
bool
byte
int
int32
float
float32
char
string
error
type
ObjectName
EnumName
EngineTypeName
const ObjectName
const EngineTypeName
task<Type>
array<Type>
slice<Type>
map<KeyType, ValueType>
channel<Type>
rows<Query, Access>
view<Query, Access>
borrow<Access.field>
ReferenceType?
fn(Type, OtherType) -> ResultType
infallible fn(Type, OtherType) -> ResultType
Only reference types may take ?. The parameterized types are built in; there
are no user-defined generic types or functions.
const makes a shallow read-only view of an object, interface, or engine type.
It is rejected on scalars, collections, and function types. A mutable value may
be passed where a const type is expected. A const value cannot be assigned,
returned, captured, or passed as its mutable type.
Function types are scalar and non-nullable. A plain fn type is fallible;
infallible fn is not.
rows, view, and borrow give a system access to its entities' components
during one call. They may be locals and parameters. They cannot be null, stored
in objects or containers, returned, or used in a task.
Query and access declarations
query Motion {
Position as position;
Velocity as velocity;
}
access Integrate for Motion {
write position;
read velocity;
}
Each query field is a component an entity must have. as is optional; the field name
defaults to the type name. A profile names every query field exactly once, with
read or write. See Component views.
Type reflection
type descriptor = typeof(ObjectName);
type collection = typeof(array<int>);
The operand of typeof is type syntax, resolved at compile time. An unknown
type is a compile-time error, even when a variable has that spelling. type
values may be stored, passed, returned, and compared with == and !=. They
are non-nullable and support no ordering or arithmetic. See
Type reflection.
Comments
// Line comment
/* Block comment */
/* Block comments may /* nest */ safely. */
/// Doc comment: documents the declaration on the next line.
/// Markdown is kept as written.
fn documented() {}
A #! line at the very start of a file is skipped.
A run of /// lines directly above a declaration documents it. A blank line,
an ordinary // line, or a //// banner ends the run, and /// after code on
the same line is an ordinary comment. Each line loses /// and one following
space. See Doc comments.
Literals
true
false
null
123
1.25
2e10
1.5e-3
NaN
Infinity
'a'
"text\n"
"hp ${hp} of ${max}"
[1, 2, 3]
{"one": 1, "two": 2}
Point { x: 1.0, y: 2.0 }
-123 is unary minus applied to 123. The largest literal is
9223372036854775807; -9223372036854775808 (written directly after -) is
the minimum int. -Infinity is unary minus applied to Infinity. There are
no hexadecimal, binary, or octal literals.
An array literal assigned, passed, or returned as array<E> takes that element
type and converts each element: array<float> a = [1, 2];.
An empty [] or {} literal cannot infer its types. Use new array<T>() or
new map<K, V>().
A single trailing comma is accepted in parameter lists, argument lists, and array, map, and object literals. It is not accepted in type argument lists.
Object declarations
object Name {
Type public_field;
~Type private_field;
infallible fn method(Type parameter) -> ResultType {
statements
}
}
A ~ field is private to the package that declares the object. Code in other
packages cannot read, write, or initialize it. Methods receive the object as
this and are called as value.method(...).
An object literal must initialize every non-null reference and string field.
Omitted numeric and Boolean fields are zero or false; omitted nullable fields
are null.
Interface declarations
interface Name {
infallible fn inspect() -> int;
fn update(int value) -> void;
}
An object satisfies an interface structurally, with no implements clause. It
must provide every method with matching parameter and result types. An
infallible method satisfies a fallible requirement; a fallible method does not
satisfy an infallible one.
Enum declarations
enum Direction { North, East, South, West }
enum Layer : byte { Ground = 1, Air = 2 }
Members count up from 0, or from the previous member's value. The backing
type after : is int (default), int32 or byte. Write a member as
Direction.North. An enum never converts to or from a number implicitly:
int(d) gives the value and Direction(n)? the member, failing when n is
no member's value. Same-enum values support ==, !=, <, <=, >, >=.
Function declarations
infallible fn name(Type parameter, OtherType other) -> ResultType {
statements
}
infallible fn no_result(Type parameter) {
statements
}
fn name(Type parameter) -> ResultType {
statements
}
tree name(Type parameter) -> ResultType {
statements
}
A fn is fallible unless marked infallible. A tree is always fallible.
Omitting -> Type means -> void.
A top-level function, tree, or library function name is a value. It may be
stored, passed, returned, compared with == and !=, called, or branched.
Calls through a function value are positional. BT has no closures, lambdas, or
bound-method values.
Variable declarations
Type name;
Type name = expression;
Every variable names its type. There is no var, let, or auto.
Blocks and statements
{
statement
statement
}
Statement forms:
Type variable = expression;
expression;
throw expression;
if (condition) statement
if (condition) statement else statement
switch (value) {
1, 2 => { statements } // constant patterns
Enum.Member => statement
Type name => { statements } // type pattern binds `name`
_ => { statements } // anything else; last
}
while (condition) statement
repeat (count) statement
for (Type item in collection) statement // array, slice, map keys, string chars, or query rows
for (int i, Type item in sequence) statement // index and element
for (K key, V value in map) statement // key and value; unspecified order
for (Type i = start; condition; step) statement
for (; condition; ) statement // any clause may be omitted
for statement // no parentheses: loops forever
variable++;
variable--;
target += expression; // also -=, *=, /=, %=
source => destination; // bulk copy
call() !! { statements }; // handle a failure; `err` is bound
{ statements } !! { statements }; // the same, guarding a block
call() && { statements }; // defer until the scope exits
{ statements } && { statements }; // defer, scoped to that block
{ statements } && { } !! { }; // chained left to right
break;
continue;
return;
return expression;
select {
Type value = channel.receive() { statements }
Type other = another.receive() { statements }
else { statements }
}
select {
Type value = channel.receive() { statements }
} unless guard;
break and continue apply to the nearest loop and cannot leave a
branch block. A lone ; is not a statement. A for-in variable takes no
initializer.
An expression statement may ignore a non-void result. discard expression; is
still accepted but deprecated.
In a counted for, an init declaration is scoped to the loop, an omitted
condition is true, and continue runs the step before the next test.
++ and -- are statements: i++ is i += 1 and has no value, so
int j = i++; and f(i++) are errors. The target is a variable, field, or
element, and is evaluated once: a[f()]++ calls f once. A compound
assignment x op= v also evaluates its target once.
A trailing && { } runs its block when the guarded part's scope exits, on
every path including failure. A deferred block must finish normally: ?,
throw, return, break, and continue are not allowed inside it.
&& starts a deferred block only when { follows it directly; a && b is
logical AND. See Error handling for chaining.
switch runs the first arm whose patterns match the value, which is
evaluated once; there is no fallthrough. Patterns are literals, enum members
and const names over an integer, char, bool, string or enum value, or
Type name over an object, interface or engine object value. A pattern
appears once, _ comes last, and a switch over an enum without _ must list
every member. break and continue in an arm apply to the enclosing loop.
See Statements.
select waits until one arm's channel delivers and binds the received value
for that arm. Arms are polled in source order, so earlier arms win ties.
Without else, a closed channel fails the select like a bare receive(),
so the enclosing function must be fallible. A trailing else makes it a
single non-blocking poll that runs else when no arm holds a value, closed
channels included; it never fails. select { ... } unless guard; also fails
with -305 when the channel guard fires (holds a value or closes) before any
arm is ready; it cannot have an else. See Concurrency.
Calls
function(first, second)
function(first: value, second: other)
function(value, second: other)
Each parameter receives exactly one argument. Positional and named arguments may be mixed. Built-in array and channel operations and function values take positional arguments only. A method called through an interface runs the concrete object's implementation.
Member and index access
object.field
object.field = value
array[index]
array[index] = value
array[low:high]
array[:high]
array[low:]
array[:]
destination[low:high] = source[low:high]
source => destination
map[key] // fallible read: a missing key raises an error
map[key] = value // infallible write: inserts or overwrites
map ?? key // bool: true if key is present
A map read is fallible. Consume it with ?, handle it with !! { }, or guard
it with if (map ?? key) { ... }, which makes a read of that key inside the
block infallible. Array and slice reads are not fallible; an out-of-range
index traps.
Slice bounds are half-open and must satisfy 0 <= low <= high <= length;
reslicing a slice may reach up to its capacity.
Array and slice ranges produce slice<T>; buffer ranges produce
slice<byte>. Range assignment and source => destination copy exactly
matching lengths and permit overlap. The arrow copies between arrays and slices
of the same element type, and between buffers and array<byte> or
slice<byte>. Member access, indexing, and slicing chain where the types
allow.
Construction
Source objects use object literals:
Point { x: 1.0, y: 2.0 }
Collections, buffers, and errors use new:
new array<int>()
new array<int>(length)
new map<string, int>()
new channel<Message>()
new channel<Message>(capacity)
new buffer() // requires include "buffer"
new buffer(size)
new error(code, message)
Concurrency expressions
branch function(arguments)
branch {
statements
return value;
}
join task
join task unless guard
channel.receive() unless guard
branch produces task<T>. join produces the task's result and is
fallible. With unless, the wait gives up when the channel guard fires
(holds a value or closes) first: the join cancels the task, and both forms
fail with -305. Write (join task unless guard)? to propagate.
Error expressions
Propagate a failure:
function(arguments)?
(join task)?
Handle it locally:
function(arguments) !! {
// err is the error
statements
};
Handle it inside concurrent work:
task<void> task = branch function(arguments) !! {
statements
};
A handler recovers by falling through or rethrows with throw err;. A handled
expression has type void: it discards the successful result.
Operators and precedence
From tightest to loosest:
| Precedence | Forms | Association |
|---|---|---|
| 1 | calls, ., indexing and slicing, postfix ?, postfix !! | left |
| 2 | join, branch, unary +, unary -, ! | right |
| 3 | unless | none |
| 4 | *, /, % | left |
| 5 | +, - | left |
| 6 | <, <=, >, >=, is | left; is does not chain |
| 7 | ==, != | left |
| 8 | ?? (map key presence) | left |
| 9 | && | left, short-circuiting |
| 10 | || | left, short-circuiting |
| 11 | =, +=, -=, *=, /=, %= | right |
source => destination; is a statement, not an expression.
switch (value) { 1 => a, 2, 3 => b, _ => c } is an expression: arms are
separated by commas, every arm has one type, and the arms must cover every
value (_, every enum member, or both bool values).
value is Type tests an object or interface value's actual type, and
Type(value)? downcasts it, failing when the value is not a Type.
A call to a type name converts: int(f), int32(n), byte(n),
float32(x) and char(n) narrow a number, int(e) gives an enum's value,
and Enum(n)? finds a member, failing when there is none. See
Conversions.
An assignment target is a variable, writable field, array or slice element,
map element, or writable range. Assigning to a range copies elements.
Assigning to a slice<T> variable rebinds it.
Not in BT
try,catch, andfinally. Trailing!! { }and&& { }blocks handle failures and run cleanup.printf-style width and padding. Interpolate with"${x}", and round withfloat_to_string.- A ternary operator. Use a
switchexpression orif/else. - Bitwise operators and prefix
++x. - Constructor declarations and inheritance.
- User-defined generics, closures, bound-method values, and lambdas.
- Map deletion.
for-in over channels and buffers.selectsend arms.- Manual memory allocation and deallocation.
cTurtle adds types and functions through packages. It does not change the syntax.