Skip to main content

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:

PrecedenceFormsAssociation
1calls, ., indexing and slicing, postfix ?, postfix !!left
2join, branch, unary +, unary -, !right
3unlessnone
4*, /, %left
5+, -left
6<, <=, >, >=, isleft; 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, and finally. Trailing !! { } and && { } blocks handle failures and run cleanup.
  • printf-style width and padding. Interpolate with "${x}", and round with float_to_string.
  • A ternary operator. Use a switch expression or if/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.
  • select send arms.
  • Manual memory allocation and deallocation.

cTurtle adds types and functions through packages. It does not change the syntax.