Skip to main content

Language Basics

BT is case-sensitive. Statements end with semicolons, and braces delimit blocks.

The examples in this chapter are statements inside a function body. Those that call println assume include "io";, and those that call int_to_string assume include "string";.

Comments​

Line comments begin with //. Block comments use /* and */ and may nest:

// This comment ends at the end of the line.
int answer = 42;

/* Outer comment
/* nested comment */
*/

A doc comment is one or more /// lines directly above a declaration. Editors show it when you hover over a use of the declaration, and generated API references include it. The text is markdown. Unlike the other examples in this chapter, this one is top-level code:

/// Returns the larger of `a` and `b`.
///
/// Equal values return `a`.
infallible fn larger(int a, int b) -> int {
if (a >= b) { return a; }
return b;
}

object Tally {
/// The running total.
int total;
}

The /// lines must touch the declaration: a blank line or an ordinary // comment in between detaches them. Fields, methods, and parameters written on their own lines take doc comments too. See Doc comments for the exact rules.

Built-in types​

Built-in type names are lowercase.

TypeMeaning
voidNo value
booltrue or false
byte0 to 255; array<byte> stores one byte per element
intSigned 64-bit integer, the default integer type
int32Signed 32-bit integer; arithmetic wraps
float64-bit floating-point number, the default float type
float3232-bit floating-point number; arithmetic is single precision
charOne Unicode code point, written 'a'
stringImmutable text
errorAn error code and message
typeA compile-time type descriptor
task<T>The eventual result of concurrent work
array<T>Growable indexed collection
slice<T>A view of a range of an array or buffer
map<K, V>Key-value collection
channel<T>Typed communication channel

Programs define their own object, interface and enum types. Packages such as buffer and cTurtle's own packages add more.

typeof(T) takes type syntax and produces a type value at compile time. Its operand is never a variable:

object Character { int health; }

type characterType = typeof(Character);
type scoresType = typeof(array<int>);

Only reference types may be nullable. Add ? to the type: Character?, array<int>?.

Literals​

bool enabled = true;
int lives = 3;
float ratio = 1.5;
string title = "Starwhiz";
Character? target = null;

Integer literals are decimal. Float literals use a fraction, an exponent, or both:

float a = 0.25;
float b = 2e3;
float c = 1.5e-2;
float missing = NaN;
float far = Infinity; // -Infinity for negative infinity

A literal takes the type its context asks for when it fits. A float literal rounds to float32; an integer literal must be in range for int32 or byte:

float32 half = 0.5;
int32 count = 12;
byte full = 255; // byte full = 256; is a compile error
float32 scaled = speed * 0.5; // speed is float32, so 0.5 is too

A literal adapts only beside a typed value or into a typed destination: an expression made only of literals, such as 1.0 / 3.0, is a float.

Strings support these escapes:

EscapeMeaning
\nNewline
\rCarriage return
\tTab
\"Double quote
\\Backslash
\$Dollar sign, so \${ is plain text

A character literal is one Unicode code point in single quotes, of type char: 'a', 'é', '\n', '\'', '\0' or '\u{1F600}' (1 to 6 hex digits). It takes the same escapes as a string plus \', \0 and \u{...}; any other escape is an error, and so is a literal with no character or more than one.

${...} inside a string inserts a value. Numbers, bool, char, enum values and strings can go inside:

int hp = 7;
string line = "hp ${hp} of ${hp * 2}"; // "hp 7 of 14"

+ joins two strings into a new one, or a string and a char: "grade " + 'A'. A number needs interpolation or a function such as int_to_string; "hp " + hp is an error. Comparison operators compare strings by byte content. Strings pass freely between tasks and through channels. See Strings for methods and number formatting. To assemble a large text piece by piece, write into a buffer and convert once with to_string; repeated + in a loop copies the accumulated text every time.

Variables​

A declaration names its type and may include an initializer. There is no type inference keyword.

int count = 0;
float speed = 12.5;
bool ready;

A variable declared without a value starts at its type's zero value: 0, 0.0, false, '\0', an empty string, or an empty array or map. An enum starts at its member valued 0, or its first member. Nullable references start as null. Other non-null references (objects, interfaces, buffers, channels, tasks, slices) have no zero value and also start as null; give them a value before use, because reading through one traps at runtime.

Assignment is an expression and associates right to left:

int a;
int b;
a = b = 4;

Numeric conversion​

int and float are 64-bit and are what BT uses unless you ask otherwise. int32 and float32 are 32-bit and opt-in. Integer arithmetic on byte values, including unary -, produces int: -b of a byte holding 5 is the int -5. 5 / 2 is 2. Equality between an integer and a float compares numeric values, so 1 == 1.0 is true.

A value converts implicitly only when the conversion cannot change it:

FromConverts implicitly to
byteint32, int, float32, float
int32int, float
charint32, int, float32, float
intfloat
float32float

int to float can round above 2^53; it stays implicit so mixed arithmetic reads naturally. Every other conversion narrows and must be written as a call to the target type:

int32 n = int32(total); // keeps the low 32 bits, sign-extended
byte b = byte(value); // keeps the low 8 bits
float32 f = float32(x); // rounds to nearest
int whole = int(seconds); // truncates toward zero
char c = char(code); // a code point, or U+FFFD if code is not one

A float converts to an integer type by truncating toward zero and saturating at the type's range; NaN converts to zero. int32 → float32 is explicit because float32 holds integers exactly only up to 2^24.

Arithmetic and comparison compute in the narrower type both operands convert to: int32 + int32 is int32, float32 * float32 is float32, int32 + int is int, float32 + float is float, and int32 + float32 is float. Integer arithmetic on byte values produces int, and on char values int32: c - 'a' is an int32, and char(c + 1) makes it a char again. 5 / 2 is 2. int32 arithmetic wraps on overflow, including INT32_MIN / -1; division by zero traps.

Operators​

CategoryOperators
Arithmetic+, -, *, /, %, unary - and +
Comparison<, <=, >, >=
Equality==, !=
Logical!, &&, ||
Map key presence??
Assignment=, +=, -=, *=, /=, %=
Error propagationpostfix ?
Error handlingpostfix !! { ... }

&& and || short-circuit. % takes integers only. Ordering operators take numbers or two strings. + takes numbers, two strings, or a string and a char. BT has no bitwise operators.

Precedence, from tightest to loosest:

  1. Member access, indexing, calls, ?, and !!
  2. Unary !, -, +, join, and branch
  3. *, /, %
  4. +, -
  5. <, <=, >, >=, is
  6. ==, !=
  7. ??
  8. &&
  9. ||
  10. =, +=, -=, *=, /=, %=

unless (see Concurrency) binds between the unary operators and *.

Use parentheses when they make intent clearer.

Compound assignment​

total += price stores total + price in total; -=, *=, /= and %= work the same way. The result must fit the target as in a plain assignment, so byte b; b += 1; is an error (b + 1 is an int). text += "!" and text += 'c' append to a string.

The target is evaluated once: in scores[next()] += 5, next() runs once and that element is read and written.

Increment and decrement​

index++ is index += 1, and index-- is index -= 1. They produce no value, so they appear only as a statement or a for step:

int index = 0;
index++;

Enums​

An enum is a type with a fixed set of named values. Declare it at the top level of a file, and name a member through the type:

enum Weapon { Sword, Bow, Staff }

Weapon held = Weapon.Bow;
if (held == Weapon.Bow) {
println("Ranged");
}
println("holding ${held}"); // holding Bow

Members count up from 0, so Bow is 1. A member may set its own value, enum Layer { Ground = 1, Air = 2 }. An enum never turns into a number on its own. Convert explicitly: int(held) gives 1, and Weapon(n)? gives the member whose value is n, failing when there is none.

Conditional execution​

if (health <= 0) {
println("Defeated");
} else if (health < 25) {
println("Wounded");
} else {
println("Ready");
}

Conditions must be bool; integers are not treated as Boolean values.

An if, while, repeat, or for body may be a single statement without braces. Braces make later edits safer.

Choosing with switch​

switch runs the first arm whose patterns match a value. An arm lists one or more constants, then => and a block or a single statement; _ matches anything else and comes last. Only one arm runs.

switch (command) {
"go", "run" => { start(); }
"stop" => { halt(); }
_ => { println("Unknown command"); }
}

Patterns can be numbers, chars, strings, true/false, enum members, or const names. A switch over an enum that has no _ arm must name every member, so adding a member later points you at each switch to update:

enum Light { Red, Amber, Green }

switch (light) {
Light.Red => { stop(); }
Light.Amber, Light.Green => { go(); }
}

Over an object or interface, a pattern names a type and a variable. The arm runs when the value is that type, with the variable holding it:

switch (shape) {
Circle c => { println("radius ${c.r}"); }
Square s => { println("side ${s.side}"); }
_ => { }
}

The expression form gives a value. Its arms are expressions separated by commas, and it must cover every case:

int damage = switch (weapon) { Weapon.Sword => 8, Weapon.Bow => 5, _ => 1 };

break and continue inside an arm apply to the loop around the switch. See Statements for the full rules.

While loops​

int index = 0;
while (index < 10) {
index++;
}

Counted loops​

A three-clause for declares, tests, and steps a counter:

for (int i = 0; i < 10; i++) {
println(int_to_string(i));
}

The counter is local to the loop. continue runs the step before testing the condition again, so it never skips the increment.

Any clause may be omitted; an omitted condition is true. A for with no parentheses loops until something breaks out:

array<int> pending = [3, 1, 2];
for (; pending.length() > 0; ) {
int job = pending.pop()?;
}

int frames = 0;
for {
frames++;
if (frames == 60) {
break;
}
}

Repeat loops​

repeat (count) runs a block a fixed number of times. The count is an int, evaluated once before the loop. A count of zero or less runs the body zero times.

repeat (3) {
println("Again");
}

Iterating collections​

for ... in visits each element of an array or slice in index order:

array<int> scores = [10, 20, 30];
for (int score in scores) {
if (score >= 20) {
println("High score");
}
}

Name two variables to get the index as well: for (int i, int score in scores) { ... }.

A map loop gives each key, or each key and value. The order is unspecified:

map<string, int> ammo = {"bolts": 40, "missiles": 4};
for (string name, int count in ammo) {
println("${name}: ${count}");
}

A string loop gives each character as a char:

int spaces = 0;
for (char c in "a b c") {
if (c == ' ') { spaces++; }
}

The loop variable is local to the body. The length is read once before the loop. A scalar loop variable holds a copy; an object loop variable holds the element's reference. Assigning to the loop variable does not change the collection; use indexing to replace an element. Do not change the collection's length during iteration.

The same syntax loops over the entities a system works on. See Component views.

Breaking and continuing​

break exits the nearest loop. continue starts its next iteration.

int wave = 0;
while (wave < 10) {
wave++;
if (wave % 2 == 0) {
continue; // skip even waves
}
if (wave > 7) {
break;
}
println("wave " + int_to_string(wave));
}

Blocks and scope​

A variable is visible from its declaration to the end of its block. Inner blocks may declare their own locals.

int outer = 1;
if (true) {
int inner = outer + 1;
}
// inner is not visible here.