Declarations
What can appear at the top level of a .bt file, and how names and scopes
work.
object Turret {
float angle;
~int shots_fired; // private to this package
infallible fn fire() { this.shots_fired = this.shots_fired + 1; }
}
interface Damageable { fn take_hit(int amount); }
infallible fn turret_create(float angle) -> Turret {
return Turret { angle: angle };
}
object Guard { float x; float speed; }
tree patrol(Guard guard) -> void {
guard.x = guard.x + guard.speed;
}
Source files
A file contains only these, in any order:
include "package/path";(see Includes);- functions (
fn,infallible fn) and trees (tree); object,interface,enum,queryandaccessdeclarations;- constants (
const T NAME = value;) and variables (T name = value;), see Constants and variables.
Any of them except include can start with ~ to make it private to its
package.
- You can use a declaration before it appears, and from any other file in the same package.
- There are no type aliases.
- Anything else at the top level is the error
expected include, object, interface, enum, fn, infallible fn, tree, constant or variable declaration.
Names belong to their package
Each package has its own name space. Two packages may declare the same name; a file picks one by its own package first, or by qualifying it with the package (see Names and namespaces).
- Declaring the same name twice in one package is
duplicate exported name 'f' (first declared in ...). A function and an object with the same name also collide. This includes private (~) names. - Reusing the name of a built-in or engine type is
type 'X' conflicts with another type; reusing an engine function's name isfunction 'f' conflicts with another callable. - These built-in names cannot be used for your own functions:
send,receive,close,sqrt,array_length,array_capacity,array_reserve,array_resize,array_push,array_pop,array_insert,array_remove,array_clear,array_fill. Methods with these names (such asfn close()inside an object) are fine.
Engine and built-in names stay global, so they can't be reused in any package.
Private declarations
~ in front of a function, tree, object, interface, enum, query, access, constant
or variable declaration makes it private to its package (its folder). The same ~ marks
object fields private.
~infallible fn clamp_index(int index, int count) -> int { ... }
~object Cursor { int line; int column; }
- Every file in the same folder can use it.
- Any use from another package is an error, whether or not that file includes
the package:
function 'f' is private to its package(and the same fortree,object,interface,enum,query,access profile,constantandglobal). - A public declaration cannot expose a private type in its parameters,
result, public fields or public methods, even inside
array<T>,T?or a function type:public function 'f' exposes private object 'Cursor'. A private (~) field can use private types. ~cannot be put on anincludeor on a method. Methods are visible wherever their object is; to keep a helper private, write it as a private top-level function that takes the object.- An entry point the engine starts by name (
main,gameMain,uiMain) cannot be private:entry point 'main' cannot be private. See Entry points.
Functions
infallible fn add(int left, int right) -> int { return left + right; }
fn load_text(string path) -> string { return file_read_text(path)?; } // may fail
infallible fn log_line(string text) { println(text); } // returns nothing
fndeclares a function that can fail: it maythrowor use?, and callers must handle or pass on its failures.infallible fndeclares a function that cannot fail. Inside it,?,throwand aselectwithoutelseare errors unless they sit inside a!!guard. Callers don't need to handle anything. See Errors.- Leaving out
-> Tmeans the function returns nothing (void). - Every parameter has a type. There are no default values, variable argument lists, out-parameters or overloading. Callers may name arguments instead (see Calls).
- Assigning to a parameter changes only the function's own copy; an object passed in is still shared with the caller.
return;is forvoidfunctions;return value;for the others.- A function with a result must not be able to reach its closing brace:
function 'NAME' can reach its end without returning a value. The same applies to methods (method 'Type.method' ...) and trees. See Reaching the end of a function.
Top-level functions can also be used as values; see Function values.
Trees
object Guard { float x; float speed; }
tree patrol(Guard guard) -> void {
guard.x = guard.x + guard.speed;
}
- A
treeis written and behaves exactly like anfnand always can fail (infallible treeis an error). - The host can start a tree by its name. Your own code can call, pass and
branchtrees like functions. - A cTurtle game's start-up script is the zero-argument tree
gameMain; see Entry points.
Objects
object Counter {
~int value;
string label;
infallible fn increment() { this.value = this.value + 1; }
infallible fn current() -> int { return this.value; }
}
Fields:
- Each field has a type and no initial value in the declaration. Fields left out of an object literal get default values (see Object literals).
- Field names must be unique (
duplicate field 'Counter.value'). - A
~field can only be read, written or set in a literal from the same package (field 'value' is private to its package). rows,viewandborrowcannot be field types.- A field may have the object's own type, for linked structures. Make it nullable, or you can never create the first one.
Methods:
- A method is a function with an extra hidden parameter
this, the object it was called on. You cannot name a parameterthis. - Methods can fail unless marked
infallible. - Inside a method, reach fields and other methods through
this:this.value,this.current(). A barecurrent()looks for a top-level function. - Call a method as
value.method(args); named arguments work. obj.methodwithout parentheses is an error: methods are not values.- If a field and a method have the same name,
value.nameis the field andvalue.name(...)is the method.
Objects have no constructors, destructors, inheritance, static members or operator overloading. Write a factory function to create and validate them:
infallible fn counter_create(string label) -> Counter {
return Counter { label: label };
}
Interfaces
interface Reportable {
infallible fn report() -> string;
fn flush(int limit);
}
- An interface lists method signatures: no bodies or fields.
- Any object with matching methods can be used as the interface; see Interfaces.
- Calling a method on an interface value runs the actual object's method.
Enums
enum Direction { North, East, South, West }
enum Layer : byte { Ground = 1, Air = 2, Water = 4 }
- An enum is its own type with a fixed set of named values, its members.
Name a member as
Direction.North. - A member without
= valueis one more than the member before it; the first is0. A value must be an integer literal, optionally negative. : int,: int32or: byteafter the name stores the values in that type; without it they areint. Every value must fit:enum member 'X' value 256 does not fit 'byte'.- Member names and values must be unique (
duplicate enum member 'X',enum members 'X' and 'Y' have the same value 1). An enum needs at least one member, and a trailing comma is allowed. - Enums have no methods or fields. See Enums for how values behave.
Query and access declarations
A query picks out the entities a system works on: every entity that has all
the listed components. An access declaration for that query says which of
those components the system only reads and which it also writes. The guide's
Component views shows how to use
them with cturtle/world.
object Position { float x; float y; }
object Velocity { float x; float y; }
query Motion {
Position as position;
Velocity as velocity;
}
access Integrate for Motion {
write position;
read velocity;
}
query rules:
- At least one component (
query requires at least one component). - Each component is an object (or engine component type) whose fields are
all
int,int32,float,float32,bool,byteorchar. Strings, enums, references and nested objects are not allowed. as namegives the component the name you use in the system (v.position); without it the name is the type's name. Different names let one query list the same component type twice.- Each
query Qalso gives you an object namedQBindings, which you fill in when you register the query with the engine. You can't declare that name yourself.
access rules:
fornames a query.- List every component name of the query exactly once, as
readorwrite. A missing one isaccess profile must declare 'v'; an unknown one isunknown query field in access profile. writeallows reading and writing;readallows reading only.
A system reaches the matching entities through three view types. In each, Q
is the query and A is the access declaration:
| Type | Meaning |
|---|---|
rows<Q, A> | A batch of matching entities. Loop over it with for (view<Q, A> v in rows); rows[i] can fail. |
view<Q, A> | One entity's components. v.position gives that component. |
borrow<A.name> | One component, such as borrow<Integrate.position>; writable only if the access declaration says write. |
A view is only valid while the system is working on it, so:
- Views cannot be nullable, stored in fields or collections, returned, or
captured by
branch. They can be locals and parameters. - A function that holds one cannot wait or call unknown code: no
branch,joinorselect, no calls through function values or interfaces, and no engine functions that aren't marked safe for this. Calls to your own functions are fine if they follow the same rules. Breaking this iscall or suspension is not borrow-safe while a composite view is live. Do the waiting in a separate function.
Constants and variables
const int MAX_PLAYERS = 8;
const float TICK = 1.0 / 60.0;
const string TITLE = "Space " + "Patrol";
int frames_drawn = 0;
map<string, Sprite> sprite_cache = {};
Settings settings = load_settings()?;
A top-level declaration that starts with const is a constant; one that
starts with a type is a variable. Both always have a value after =:
int count; at the top level is the error expected '=' and an initial value.
Constants:
- The type is
bool,byte,int,int32,float,float32orstring; anything else isconstant 'X' must be a bool, a number or a string. - The value is computed while compiling, from literals, other constants,
arithmetic, comparisons,
&&,||,!, string+and numeric conversions such asint32(5)andfloat32(0.5). Anything else, such as a call or a variable, is an error likethe value of constant 'X' cannot call a functionorconstant 'X' cannot use variable 'v'. - The arithmetic is the same as at run time:
int32wraps, integer division rounds toward zero, and dividing by zero isthe value of constant 'X' divides by zero. - A constant may use constants declared later, but not itself, directly or
through others:
constant 'A' is defined in terms of itself. - Assigning to one is
cannot assign to constant 'X'.
Variables:
- A variable can have any type a local can have, except the views
rows,viewandborrow. - The program has one copy of each variable, created when the program starts. Every function and every task reads and writes that same copy. Whatever a variable holds stays in memory until you assign something else to it.
- The value after
=may be any expression, including calls. A failure must be handled or passed on with?, as for a local. - Tasks share variables without any locking, exactly like object fields; see Sharing data between tasks.
Initialization order
When the program starts, before any entry point runs, every variable is set to its value:
- a package's variables are set after the variables of the packages it includes;
- inside one package, in the order the files were added, then top to bottom.
An initializer may only use variables that are set before it. Using one that
comes later, or the variable itself, is a compile error:
the initializer of 'a' uses 'b', which is initialized after it. Two packages
that include each other are set in a fixed order, so only one of them can read
the other's variables from an initializer. Functions called by an initializer
are not checked this way: a variable that is not set yet reads as zero, false,
an empty string or null.
If an initializer fails, the program does not start and no entry point runs.
The error is global initialization failed: <message>.
Local variables
int count = 0;
Turret? target;
- Every local names its type.
- A local declared without a value starts at its type's starting value.
- A value that can fail must be handled:
string text = file_read_text(path)?;.
Names and scopes
| Name | Visible in |
|---|---|
| Top-level declaration, including constants and variables | The whole program, if its package is visible to the file (see Visibility). |
Parameter, this | The function body. |
| Local variable | From its declaration to the end of its block. |
Variable declared in a for (...) header | The loop. |
for ... in variable | The loop body. |
select arm variable | That arm's block. |
switch type-pattern variable (Circle c =>) | That arm. |
err | The !! handler block. |
| Field, method | Only through a value: value.field, this.field. |
- Two locals with the same name in the same block are an error
(
duplicate local 'x'). A nested block may reuse an outer name, and a local may reuse a parameter's name. - A local or parameter hides a top-level function, constant or variable with
the same name: if
fis a local function value,f(1)calls the local. - A top-level variable or constant cannot share its name with a function,
including an engine function:
global 'log' conflicts with a function. - Type names are separate: a local named
taskdoes not hide the type. q.name, whereqis an include's alias or last path segment and no local is namedq, names a declaration of that package.E.Member, whereEis an enum, names that member.- Otherwise
A.Bis first looked up as an engine constant (for exampleAction.ScanSector); then it is a field access.