Skip to main content

Types

Every variable, parameter, field and result in BT has a type that is fixed when you write the code. You always name the type; BT has no type inference.

object Player { string name; int health; }
infallible fn add(int a, int b) -> int { return a + b; }

int lives = 3;
float speed = 4.5;
string name = "Ada";
array<int> scores = [10, 20, 30];
map<string, int> ammo = {"bolts": 40, "missiles": 4};
Player? target = null; // may be null
infallible fn(int, int) -> int combine = add; // a function value

Type syntax​

  • T? means "a T or null" (see Nullability). The ? covers the whole type: array<Player>? is an array that may be null; array<Player?> is an array whose elements may be null.
  • const T is a read-only view of an object (see Const views).
  • fn(P1, P2) -> R is a function type (see Function types).
  • Built-in type names are lowercase. Int or Array are not built-in types.

Scalars​

TypeValuesStarts as
booltrue, falsefalse
byte0 to 2550
intSigned 64-bit integer0
int32Signed 32-bit integer0
charA Unicode code point, U+0000 to U+10FFFF'\0'
float64-bit double-precision float0.0
float3232-bit single-precision float0.0
typeA type descriptor from typeof(T) (see Type reflection)—
voidNo value. Used only as a result type; leaving out -> T means void.—

int and float are the defaults; int32 and float32 are opt-in, mainly for engine data stored at 32 bits. Scalars are values: assigning one copies it. They can never be null. Overflow, division and rounding are described in Execution.

char​

char holds one Unicode code point. Write one as a character literal: 'a', '\n', '\u{1F600}'.

  • A char converts to int32, int, float32 and float without a call. Nothing converts to char without one: char c = 65; is an error.
  • char(x) takes an integer. It keeps the value when it is a code point. Any other value becomes U+FFFD, the replacement character: a negative number, a UTF-16 surrogate (U+D800 to U+DFFF), or anything above U+10FFFF.
  • Arithmetic on chars computes in int32: c - 'a' is an int32. Turn a result back into a char with char(c + 1).
  • Chars compare by code point with ==, !=, <, <=, > and >=.
  • "x" + c and c + "x" join the char's UTF-8 text to a string. char_to_string(c) from string gives the text alone. U+0000 has no text, because a string never holds a NUL byte.
  • A char can be an object field, an array element or a map key.
  • "${c}" interpolates the char's text, and for (char c in s) or s.chars() takes a string apart into chars (see for ... in).

Enums​

An enum declaration (see Enums) defines a type whose values are its members.

enum Direction { North, East, South, West }

Direction facing = Direction.East;
if (facing == Direction.East) { ... }
int code = int(facing); // 1
Direction back = Direction(code)?; // fails unless code is a member's value
  • An enum is a value, like a number. It can never be null and cannot be const.
  • It never converts to or from a number on its own: int n = facing; and Direction d = 1; are errors. Write int(facing), int32(facing) or byte(facing) to get the value; narrowing keeps the low bits as usual. float(facing) is an error.
  • Direction(n) turns an integer into a member. It can fail: when n is not the value of a member it fails with code 3 (not a value of enum Direction), so write Direction(n)? or handle it with !!. A literal that is not a member's value is a compile error.
  • == and != compare two values of the same enum. <, <=, > and >= compare their values. Arithmetic is not allowed.
  • Enums work as fields, parameters, results, array elements and map keys.
  • An enum starts as its member valued 0, or its first member when no member is 0. An enum with no member valued 0 has no zero value, so new array<E>(n), resize, and leaving such a field out of an object literal are errors, and a slice of such an enum cannot be resliced past its length (see Slices).
  • "${facing}" interpolates the member's name, East.
  • From another package, name an enum's members through the package: geo.Direction.North, and convert with geo.Direction(n)?.

Strings​

string is an immutable sequence of bytes, normally UTF-8 text.

  • It is a value: it is never null and cannot be string? or const. An uninitialized string is "".
  • + joins two strings. ==, !=, <, <=, >, >= compare contents byte by byte.
  • You cannot index or slice a string with []. Use string methods such as length, slice, find, byte_at and chars.
  • for (char c in s) visits each Unicode code point; see for ... in.
  • Numbers don't convert to strings with +. Use interpolation, "HP: ${hp}", or a function such as int_to_string(hp).

Built-in generic types​

BT has a fixed set of types that take type arguments. You cannot declare your own generic types or functions.

TypeMeaning
array<T>Growable list, indexed from 0.
slice<T>Window onto part of an array or buffer; grows with push and extend.
map<K, V>Hash map from keys to values.
channel<T>Queue for passing values between tasks; see Channels.
task<T>The eventual result of branch or of a waiting engine call; see Tasks and join. task<void> has no result.
component<T>A handle to one ECS component of type T (from cturtle/world).
rows<Q, A>, view<Q, A>, borrow<A.field>Views of the entities an ECS query matches, used inside systems; see Query and access declarations.

A wrong number of type arguments is an error (type 'map' expects 2 type arguments).

Arrays​

array<int> xs = [3, 1, 2];
xs.push(4);
int first = xs[0];
array<Player?> slots = new array<Player?>(8); // 8 nulls
  • Arrays are references: assigning an array shares it, it does not copy it. Use the copy statement to copy elements.
  • Create one with a literal or new array<T>() / new array<T>(length).
  • new array<T>(n) and resize fill new slots with zero values. For an element type that cannot be null (an object, for example) there is no zero value, so these are compile errors; use a nullable element type such as array<Enemy?>.
  • Reading or writing past the end traps.
  • Methods: length, capacity, reserve, resize, fill, push, pop, insert, remove, clear. pop, insert and remove can fail. See Built-in operations.
  • An uninitialized array<T> local is a new empty array.

Slices​

slice<int> middle = xs[1:3]; // elements 1 and 2
  • Make a slice with a[low:high]; either bound can be left out. low is included, high is not.
  • A slice's capacity is the room from its first element to the end of the storage behind it (the array's or buffer's capacity). Slicing an array or buffer needs 0 <= low <= high <= length; reslicing a slice may reach into its spare capacity, 0 <= low <= high <= capacity. Other bounds trap.
  • Spare capacity holds zeros, which are not a value of every element type. A slice whose element type has no zero value (an object or other reference that is not ?, or an enum with no member valued 0) can only be resliced within its length; going past it traps, even when the storage there holds real elements.
  • Slicing a buffer gives slice<byte>.
  • Methods: length(), capacity(), push(x) and extend(sequence), where the sequence is an array or slice with the same element type.
  • push and extend write into the storage behind the slice while its capacity allows. That overwrites whatever the array or another slice holds there, as in Go. When the capacity runs out, the slice moves to new storage about twice the size. Slices are references, so everything holding that slice sees the move; other slices, and the array, keep the old storage.
  • A slice keeps viewing the storage it was made from. If the array later grows into new storage, the slice still sees the old storage.

Maps​

map<string, int> ammo = new map<string, int>();
ammo["bolts"] = 40; // insert or overwrite
if (ammo ?? "bolts") { int n = ammo["bolts"]; } // checked: cannot fail
int m = ammo["missiles"]?; // fails if missing
  • Maps are references; assigning one shares it.
  • Reading a key that isn't there fails (code 1, map key not found), so a read needs ? or !!, unless it is inside if (m ?? k) (see if). Writing never fails. m ?? k tests whether k is present.
  • string keys compare by content. Other keys compare by value, objects by identity, and float keys by exact bit pattern (0.0 and -0.0 are different keys).
  • Methods: length() is the number of keys; keys() and values() return new arrays of the keys and the values, in the same order as a for ... in loop, so keys()[i] belongs with values()[i].
  • Loop over keys with for (K k in m), or keys and values with for (K k, V v in m); see for ... in. The order is unspecified.
  • Maps have no deletion.
  • An uninitialized map<K, V> local is a new empty map.

Buffers​

buffer is a growable byte array with a read/write cursor, from buffer (include that package to use it). Create one with new buffer() or new buffer(size), slice it with b[low:high] to get a slice<byte>, and copy into or out of it with =>. Its methods are in the standard library reference.

The error type​

error is the value carried by a failure. It has two read-only fields, code (int) and message (string), and is created with new error(code, message). It is the only type throw accepts. See Errors.

Objects​

An object declaration defines your own type with fields and methods (see Objects).

Player p = Player { name: "Ada", health: 100 };
Player same = p; // same object, not a copy
same.health = 50; // p.health is now 50 too
  • Create objects with an object literal, Name { field: value, ... }. new Name() is an error.
  • Objects are references: assignment and parameter passing share the same object.
  • Objects may refer to each other in cycles; unused cycles are freed automatically.

Interfaces​

An interface lists methods. Any object that has methods with matching names and signatures can be used as that interface; you don't declare that it implements it.

interface Damageable { fn take_hit(int amount); }

object Crate { int hp; fn take_hit(int amount) { this.hp = this.hp - amount; } }

fn hit_all(array<Damageable> targets) {
for (Damageable target in targets) { target.take_hit(5)?; }
}
  • Parameter and result types must match exactly.
  • An infallible method can satisfy a fallible requirement, but not the other way round.
  • Converting an object to an interface shares the same object.
  • value is T tests the actual type of an interface or object value, and T(value) converts it back, failing when it is not a T. See Type tests and downcasts.
  • Most engine object types can satisfy interfaces too.

Engine types​

Engine packages add their own types once you include the package, for example Entity from cturtle/world.

  • Most engine types behave like objects: they are references, can be nullable, and have fields and methods.
  • Some engine fields describe data that can disappear while your script holds the handle (for example a component whose entity was destroyed). Reading or writing such a field can fail, so it needs ? or !!. The cTurtle API reference marks them.

Integer handles​

Some engine types are integers with their own name, such as Entity. They stop you from mixing up an entity with an ordinary number.

  • A handle converts only to its own type, never to or from int.
  • Arithmetic and </> are not allowed. == and != work between two handles of the same type.
  • Handles cannot be nullable.
  • The engine provides conversion functions where they make sense (for example Entity(id) and entity.id()).

Function types​

fn(P1, P2) -> R is the type of a function that may fail; infallible fn(P1, P2) -> R is one that cannot.

infallible fn ease_in_out(float t) -> float { return t * t * (3.0 - 2.0 * t); }

infallible fn(float) -> float easing = ease_in_out;
float y = easing(0.5);
  • The result type is required: write fn(int) -> void, not fn(int).
  • Parameters are listed by type only, so calls through a function value are positional.
  • Function values are never null and cannot be ? or const.
  • See Function values.

Nullability​

T? admits null as well as the values of T.

Can be nullableNever nullable
objects, interfaces, engine object types, error, array, slice, map, channel, task, componentbool, byte, int, int32, char, float, float32, type, string, enums, function types, integer handles, rows, view, borrow

int? and string? are compile errors (scalar type 'string' cannot be nullable). Use a sentinel value, a separate bool, or a small object.

Rules:

  • null can only be assigned to a nullable type.
  • A T can be assigned to a T?, but a T? cannot be assigned to a T.
  • Checking for null does not change the type: after if (p != null), p is still T?. You can still use its fields and methods.
  • You may use fields, methods and indexing on a nullable value without a check. If the value is null at runtime, the task traps. This includes length() on a null array?: it traps rather than returning 0.
  • A non-null object (or interface, slice, channel, task, error, engine type) local declared without a value starts out null anyway, and using it traps. Always initialize such locals.

Const views​

const T is a read-only view of an object, interface, engine object type or component<T>.

infallible fn health_of(const Player p) -> int { return p.health; } // ok
infallible fn reset(const Player p) { p.health = 0; } // error
  • You can pass a T where a const T is expected, but never the reverse.
  • Fields cannot be assigned through a const view.
  • Methods you declare in BT cannot be called through a const view. Engine methods that only read can be.
  • const is shallow: an object reached through a field of a const view is not itself const.
  • const on any other type (numbers, strings, arrays, functions) is an error.

Fallibility is not a type​

There are no result or option types. Whether something can fail belongs to the function: fn can fail, infallible fn cannot. A call to a fallible function produces its normal value on success, and you must pass on or handle the failure. See Errors.

Type reflection​

typeof(T) gives a value of type type that names a type. You mostly use it to tell engine functions which type to work with:

include "cturtle/events";

object Ping { int n; }

eventRegister(typeof(Ping), eventFifo())?;
channel<Ping> pings = eventSubscribe(typeof(Ping), 4)?;
  • The operand must be a type name known at compile time.
  • Type arguments, ? and const count: typeof(array<int>) is not equal to typeof(array<float>), and typeof(Player?) is not typeof(Player).
  • type values support ==, !=, assignment, parameters, fields and returns. You cannot get a type's name or list its fields.
  • Don't save a type value to a file; it is only meaningful while the program runs.

Starting values​

A local declared without a value starts as:

TypeStarts as
bool, byte, int, int32, float, float32false / 0 / 0.0
char'\0'
an enumits member valued 0, else its first member
string""
array<T>, map<K, V>a new empty collection
any nullable typenull
other references (objects, interfaces, error, slices, channels, tasks, engine types)null, even though the type is non-null; initialize these

Object literal fields have their own rules; see Object literals.

Conversions​

A value converts automatically, when you assign, pass an argument, return a value, or compare with ==/!=, only when the conversion cannot change it:

FromToRule
byteint32, int, float32, floatValue kept.
int32int, floatValue kept.
charint32, int, float32, floatThe code point is kept.
intfloatExact up to 2^53; larger integers round to the nearest float.
float32floatValue kept.
TT?Always.
Tconst TAlways.
object or interfaceinterface IWhen it has I's methods. A nullable value needs a nullable target.
infallible fn(...) -> Rfn(...) -> RSame parameter and result types. Not the reverse.
nullT?Only to nullable types.

A number literal takes the type it is assigned to when its value fits: int32 n = 5; and float32 f = 0.1; are fine, byte b = 300; is an error.

Every other numeric conversion narrows, so you write it as a call to the target type:

CallResult
int32(x), byte(x) from an integerKeeps the low 32 or 8 bits: byte(300) is 44, byte(-1) is 255.
float32(x)Rounds to the nearest float32.
int(f), int32(f), byte(f) from a floatTruncates toward zero and saturates at the target's range; NaN gives 0.
char(x) from an integerKeeps a code point; any other value gives U+FFFD. A float needs int(f) first.
byte(c) from a charKeeps the low 8 bits.
int(e), int32(e), byte(e) from an enumThe member's value, narrowed like an integer.

Two conversions can fail, so they need ? or !!. Both fail with code 3:

CallResult
E(n) for an enum E and an integer nThe member whose value is n; fails when there is none.
T(value) for an object, interface or engine object type TThe same value typed as T; fails when it is null or not a T. A const value gives a const T.

Everything else needs exactly the same type, including type arguments: array<int> cannot be assigned to array<float>, and array<Dog> cannot be assigned to array<Animal>.

There is no conversion from bool to anything, or between numbers and strings. Use library functions: int_to_string, float_to_string, char_to_string, int_from_string, float_from_string (string).

Arithmetic result types​

OperandsResult
byte with byteint
int32 with int32 or byteint32, wrapping on overflow
int with any integerint
float32 with float32 or bytefloat32
float32 with int32 or int, or either operand floatfloat
char with char, byte or int32int32
char with intint
char with float32float32
string + string, string + char, char + stringstring

Unary - keeps an int or float type; on a byte it gives an int (-b is 0 - b), and on a char an int32.

Equality​

a == b is allowed when one side's type can be converted to the other's, or when one side is null.

OperandsCompares
bool, byte, int, char, integer handles, enums, typevalue
floatvalue (NaN != NaN, 0.0 == -0.0)
a number with a floatnumeric value: 1 == 1.0 is true
stringcontents
objects, interfaces, error, arrays, slices, maps, channels, tasks, engine objectsidentity: whether both are the same object
function valueswhether both are the same function
anything with nullwhether it is null

Two arrays with the same contents are not equal unless they are the same array. An object and an interface holding that object are equal.

See also​