Functions and Modules
Functions are the basic unit of reusable BT code. Parameter types are explicit.
Functions are fallible unless marked infallible.
Defining and calling functions
infallible fn add(int left, int right) -> int {
return left + right;
}
infallible fn main() -> int {
int total = add(2, 3);
return total;
}
A function with no result returns void. The -> void may be omitted:
include "io";
infallible fn announce(string message) {
println(message);
}
A void function may use return; or reach the end of its body. A function
with a result must return a value on every path, or the compiler reports
function 'NAME' can reach its end without returning a value:
infallible fn clamp(int value) -> int {
if (value < 0) {
return 0;
} else if (value > 100) {
return 100;
}
// Without this return the end is reachable and the check fails.
return value;
}
The check is simple. An if without else can always be skipped, and a loop
counts as never finishing only when it is while (true) or for (;;) with no
break for it. A switch arm list that covers every value (with _ or every
enum member), a select, and throw also work when every path in them
returns or throws. The statements reference
lists every rule.
Parameters and arguments
Arguments may be positional, named, or mixed. Each parameter must be supplied exactly once.
infallible fn move_to(float x, float y, float speed) -> void {
// ...
}
infallible fn demo() {
move_to(10.0, 20.0, 5.0);
move_to(x: 10.0, y: 20.0, speed: 5.0);
move_to(10.0, speed: 5.0, y: 20.0);
}
Named arguments help when several parameters share a type. Calls to object methods accept them too. Built-in collection and channel methods, and calls through function values, take positional arguments only.
BT has no default arguments, variadic arguments, or overloading. Two top-level functions with the same name in one package are a duplicate-name error.
Unused results
A call statement may leave its result unused:
int_to_string(42); // infallible: nothing to handle
file_read_text("notes.txt")?; // can fail: the failure still needs `?`
An unused result never excuses a failure: a fallible call still needs ? or
!!. The discard keyword is deprecated; the compiler warns and asks for a
plain expression statement.
Function values
Any top-level function, including trees and library functions, can be stored, passed, returned and called through a value:
infallible fn add(int a, int b) -> int { return a + b; }
infallible fn apply(
infallible fn(int, int) -> int callback,
int a,
int b
) -> int {
return callback(a, b);
}
fn demo() -> int {
infallible fn(int, int) -> int callback = add;
task<int> later = branch callback(19, 23);
return apply(callback, 19, 23) + (join later)?;
}
fn(...) -> T is a fallible signature and infallible fn(...) -> T an
infallible one. An infallible function can be stored in a fallible signature,
but not the reverse. Indirect calls are positional and may be branched.
Function values compare by identity. They do not capture locals. Object methods cannot be extracted as bound values.
Fallibility
An ordinary function may propagate or throw errors:
include "buffer";
fn encode_settings(string heading) -> buffer {
buffer data = buffer_with_capacity(256)?;
data.append_string(heading)?;
data.append_string("\n")?;
return data;
}
The return type describes the successful result. Callers must propagate or handle a possible error; see error handling.
An infallible function cannot use ?, throw, or return an unhandled
fallible expression. Calls to it need no error handling.
Trees
A tree is an entry point that cTurtle starts, such as your game's gameMain:
object Guard {
int waypoint;
}
fn advance(Guard guard) -> void {
if (guard.waypoint < 0) {
throw new error(1, "guard is lost");
}
guard.waypoint = guard.waypoint + 1;
}
tree patrol(Guard guard) {
advance(guard)?;
advance(guard)?;
}
Trees are always fallible; infallible tree is invalid. cTurtle decides when
a tree starts. A standalone program built with ctbt uses an ordinary main
function instead.
Constants and global variables
A file can declare constants and variables next to its functions:
const int GRID_SIZE = 64;
const float CELL = 1.0 / GRID_SIZE;
const string SAVE_FILE = "save" + ".dat";
int frames_drawn = 0;
array<Enemy> enemies = new array<Enemy>();
infallible fn draw_frame() {
frames_drawn += 1;
}
A constant (const) holds a bool, number or string computed while
compiling, from literals, other constants, arithmetic, comparisons and
conversions such as float32(0.5). Every use is replaced by its value, and it
can never be assigned.
A variable has one copy for the whole program. Any function or task can read and change it. It can hold any value a local can, such as an object, an array or a function value, and its initializer may call functions:
Settings settings = load_settings()?;
When the program starts, before any entry point runs, it sets every variable in order: a package's variables after those of the packages it includes, then from the top of each file down. An initializer that uses a variable set later is a compile error. If an initializer fails, the program does not start and reports the error.
Tasks share variables without locking, like the fields of a shared object. See Shared mutable state.
~ makes a constant or variable private to its package, like any other
top-level declaration. The full rules are in
Declarations.
Packages and includes
A package is a folder. Every .bt file directly inside it belongs to the
package, and those files see each other's declarations without includes. A
nested folder is a separate package. A package's name is its module path: the
module name followed by the folder's path inside the module (see
Module definitions).
example/game/ module "example/game"
combat/actions.bt package "example/game/combat"
combat/damage.bt package "example/game/combat"
combat/ai/planner.bt package "example/game/combat/ai"
An include names a package by its module path:
include "math";
include "io";
include "example/game/combat";
An include never names a file or a relative path. include "actions.bt" and
include "../shared" are errors. Standard-library and engine packages, such as
math and cturtle/world, are included the same way.
Each file must include every outside package it uses. Includes are not
transitive: including example/game/combat does not expose the packages
combat includes. The rule covers functions, function values, types,
constants, and methods that libraries add to built-in types. Includes
conventionally come before other declarations.
// example/game/navigation/distance.bt
infallible fn distance_squared(float dx, float dy) -> float {
return dx * dx + dy * dy;
}
// example/game/actors/actor.bt
include "example/game/navigation";
infallible fn is_near(float dx, float dy) -> bool {
return distance_squared(dx, dy) < 4.0;
}
Core types such as int, string, array<T>, map<K, V>, and channel<T>
and their language-defined operations need no include. Library types and
functions do: buffer needs buffer, string methods need
string, and math functions such as sqrt need math.
Including a package that can't be found is a compile error. Two packages may include each other; each still sees only the packages it includes itself.
Private declarations
Everything a package declares is public to files that include it, unless it
starts with ~. A ~ function, tree, object, interface, enum, constant,
variable, query or access declaration is private to its package, exactly like a ~ field: every file
of the package may use it, and other packages cannot name it at all.
// example/game/navigation/distance.bt
~infallible fn square(float value) -> float { return value * value; }
infallible fn distance_squared(float dx, float dy) -> float {
return square(dx) + square(dy);
}
// example/game/navigation/heading.bt: same package, so `square` is usable
infallible fn heading_weight(float turn) -> float { return square(turn); }
From example/game/actors, calling square is
function 'square' is private to its package, even with the include.
Private helpers stay out of the API reference and out of other packages'
completions. A public function cannot take or return a private object (or
store one in a public field), since callers could not name its type; keep
such fields ~ or make the type public. Entry points such as main cannot be
private. See Private declarations
for every rule.
Module definitions
A module maps a name to a directory. A standalone module has bt.module.json
at its root:
{
"name": "example/game",
"dependencies": {
"example/widgets": "../widgets"
}
}
The module directory is the one containing the definition. Dependency paths are relative to it. Dependencies map names to local directories; BT does not download dependencies or select versions.
An include resolves from the root of the module whose name it starts with,
wherever the including file is. Names match whole path segments, and the
longest matching name wins. An include cannot escape its root through ...
A game manifest can hold the same definition in a top-level module object.
Its root is the manifest's directory. This covers both gameplay and UI
sources.
{
"module": { "name": "example/game" },
"bt": { "sources": ["bt"] },
"ui": { "entry": "ui/main.bt" }
}
Each bt.sources entry is a path relative to the manifest. It names a package
folder, or a file standing for its folder; either loads that package and the
packages it includes. ui.entry is a file whose package starts the UI.
Tools start at the nearest directory with bt.module.json or a game.json
that has a module object, then read every enclosing definition. The nearest
definition of a name wins; outer definitions add only names not yet defined.
The language server also reads its --manifest path. ctbt and
ct-game-compile accept --module-file FILE for a definition or manifest
with another filename.
Names
Each package has its own namespace. Two packages may both declare helper or
Vec2; within one package a name is declared once.
A file finds an unqualified name in its own package first, then in the
packages it includes. When two included packages declare it, qualify it with
the package's last path segment, or with an alias given by as:
include "my-game/geometry";
include "my-game/shapes" as sh;
infallible fn demo() -> int {
geometry.Vec2 point = geometry.Vec2 { x: 1, y: 2 };
sh.Vec2 box = sh.Vec2 { x: 3 };
return geometry.helper() + sh.helper();
}
Qualified names work for calls, function values, types and object literals,
enum members (sh.Tone.Soft) and conversions (sh.Tone(n)?), constants and
top-level variables (sh.counter += 1;), and engine packages:
include "math"; allows math.sqrt(x).
A local variable with the qualifier's name hides it. See
Names and namespaces.
Local variables and parameters are scoped to their function and block.