Skip to main content

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, query and access declarations;
  • 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 is function '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 as fn 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 for tree, object, interface, enum, query, access profile, constant and global).
  • 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 an include or 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
  • fn declares a function that can fail: it may throw or use ?, and callers must handle or pass on its failures.
  • infallible fn declares a function that cannot fail. Inside it, ?, throw and a select without else are errors unless they sit inside a !! guard. Callers don't need to handle anything. See Errors.
  • Leaving out -> T means 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 for void functions; 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 tree is written and behaves exactly like an fn and always can fail (infallible tree is an error).
  • The host can start a tree by its name. Your own code can call, pass and branch trees 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, view and borrow cannot 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 parameter this.
  • Methods can fail unless marked infallible.
  • Inside a method, reach fields and other methods through this: this.value, this.current(). A bare current() looks for a top-level function.
  • Call a method as value.method(args); named arguments work.
  • obj.method without parentheses is an error: methods are not values.
  • If a field and a method have the same name, value.name is the field and value.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 = value is one more than the member before it; the first is 0. A value must be an integer literal, optionally negative.
  • : int, : int32 or : byte after the name stores the values in that type; without it they are int. 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, byte or char. Strings, enums, references and nested objects are not allowed.
  • as name gives 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 Q also gives you an object named QBindings, which you fill in when you register the query with the engine. You can't declare that name yourself.

access rules:

  • for names a query.
  • List every component name of the query exactly once, as read or write. A missing one is access profile must declare 'v'; an unknown one is unknown query field in access profile.
  • write allows reading and writing; read allows reading only.

A system reaches the matching entities through three view types. In each, Q is the query and A is the access declaration:

TypeMeaning
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, join or select, 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 is call 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, float32 or string; anything else is constant '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 as int32(5) and float32(0.5). Anything else, such as a call or a variable, is an error like the value of constant 'X' cannot call a function or constant 'X' cannot use variable 'v'.
  • The arithmetic is the same as at run time: int32 wraps, integer division rounds toward zero, and dividing by zero is the 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, view and borrow.
  • 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​

NameVisible in
Top-level declaration, including constants and variablesThe whole program, if its package is visible to the file (see Visibility).
Parameter, thisThe function body.
Local variableFrom its declaration to the end of its block.
Variable declared in a for (...) headerThe loop.
for ... in variableThe loop body.
select arm variableThat arm's block.
switch type-pattern variable (Circle c =>)That arm.
errThe !! handler block.
Field, methodOnly 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 f is 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 task does not hide the type.
  • q.name, where q is an include's alias or last path segment and no local is named q, names a declaration of that package.
  • E.Member, where E is an enum, names that member.
  • Otherwise A.B is first looked up as an engine constant (for example Action.ScanSector); then it is a field access.

See also​