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 "aTornull" (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 Tis a read-only view of an object (see Const views).fn(P1, P2) -> Ris a function type (see Function types).- Built-in type names are lowercase.
IntorArrayare not built-in types.
Scalars
| Type | Values | Starts as |
|---|---|---|
bool | true, false | false |
byte | 0 to 255 | 0 |
int | Signed 64-bit integer | 0 |
int32 | Signed 32-bit integer | 0 |
char | A Unicode code point, U+0000 to U+10FFFF | '\0' |
float | 64-bit double-precision float | 0.0 |
float32 | 32-bit single-precision float | 0.0 |
type | A type descriptor from typeof(T) (see Type reflection) | — |
void | No 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,float32andfloatwithout a call. Nothing converts tocharwithout 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 anint32. Turn a result back into a char withchar(c + 1). - Chars compare by code point with
==,!=,<,<=,>and>=. "x" + candc + "x"join the char's UTF-8 text to a string.char_to_string(c)fromstringgives 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, andfor (char c in s)ors.chars()takes a string apart into chars (seefor ... 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
nulland cannot beconst. - It never converts to or from a number on its own:
int n = facing;andDirection d = 1;are errors. Writeint(facing),int32(facing)orbyte(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: whennis not the value of a member it fails with code3(not a value of enum Direction), so writeDirection(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 is0. An enum with no member valued0has no zero value, sonew 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 withgeo.Direction(n)?.
Strings
string is an immutable sequence of bytes, normally UTF-8 text.
- It is a value: it is never
nulland cannot bestring?orconst. An uninitializedstringis"". +joins two strings.==,!=,<,<=,>,>=compare contents byte by byte.- You cannot index or slice a string with
[]. Usestringmethods such aslength,slice,find,byte_atandchars. for (char c in s)visits each Unicode code point; seefor ... in.- Numbers don't convert to strings with
+. Use interpolation,"HP: ${hp}", or a function such asint_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.
| Type | Meaning |
|---|---|
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)andresizefill 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 asarray<Enemy?>.- Reading or writing past the end traps.
- Methods:
length,capacity,reserve,resize,fill,push,pop,insert,remove,clear.pop,insertandremovecan 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.lowis included,highis 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 valued0) can only be resliced within its length; going past it traps, even when the storage there holds real elements. - Slicing a
buffergivesslice<byte>. - Methods:
length(),capacity(),push(x)andextend(sequence), where the sequence is an array or slice with the same element type. pushandextendwrite 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 insideif (m ?? k)(seeif). Writing never fails.m ?? ktests whetherkis present. stringkeys compare by content. Other keys compare by value, objects by identity, andfloatkeys by exact bit pattern (0.0and-0.0are different keys).- Methods:
length()is the number of keys;keys()andvalues()return new arrays of the keys and the values, in the same order as afor ... inloop, sokeys()[i]belongs withvalues()[i]. - Loop over keys with
for (K k in m), or keys and values withfor (K k, V v in m); seefor ... 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
infalliblemethod can satisfy a fallible requirement, but not the other way round. - Converting an object to an interface shares the same object.
value is Ttests the actual type of an interface or object value, andT(value)converts it back, failing when it is not aT. 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)andentity.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, notfn(int). - Parameters are listed by type only, so calls through a function value are positional.
- Function values are never
nulland cannot be?orconst. - See Function values.
Nullability
T? admits null as well as the values of T.
| Can be nullable | Never nullable |
|---|---|
objects, interfaces, engine object types, error, array, slice, map, channel, task, component | bool, 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:
nullcan only be assigned to a nullable type.- A
Tcan be assigned to aT?, but aT?cannot be assigned to aT. - Checking for null does not change the type: after
if (p != null),pis stillT?. 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
nullat runtime, the task traps. This includeslength()on a nullarray?: it traps rather than returning 0. - A non-null object (or interface, slice, channel, task,
error, engine type) local declared without a value starts outnullanyway, 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
Twhere aconst Tis expected, but never the reverse. - Fields cannot be assigned through a
constview. - Methods you declare in BT cannot be called through a
constview. Engine methods that only read can be. constis shallow: an object reached through a field of aconstview is not itselfconst.conston 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,
?andconstcount:typeof(array<int>)is not equal totypeof(array<float>), andtypeof(Player?)is nottypeof(Player). typevalues support==,!=, assignment, parameters, fields and returns. You cannot get a type's name or list its fields.- Don't save a
typevalue to a file; it is only meaningful while the program runs.
Starting values
A local declared without a value starts as:
| Type | Starts as |
|---|---|
bool, byte, int, int32, float, float32 | false / 0 / 0.0 |
char | '\0' |
| an enum | its member valued 0, else its first member |
string | "" |
array<T>, map<K, V> | a new empty collection |
| any nullable type | null |
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:
| From | To | Rule |
|---|---|---|
byte | int32, int, float32, float | Value kept. |
int32 | int, float | Value kept. |
char | int32, int, float32, float | The code point is kept. |
int | float | Exact up to 2^53; larger integers round to the nearest float. |
float32 | float | Value kept. |
T | T? | Always. |
T | const T | Always. |
| object or interface | interface I | When it has I's methods. A nullable value needs a nullable target. |
infallible fn(...) -> R | fn(...) -> R | Same parameter and result types. Not the reverse. |
null | T? | 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:
| Call | Result |
|---|---|
int32(x), byte(x) from an integer | Keeps 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 float | Truncates toward zero and saturates at the target's range; NaN gives 0. |
char(x) from an integer | Keeps a code point; any other value gives U+FFFD. A float needs int(f) first. |
byte(c) from a char | Keeps the low 8 bits. |
int(e), int32(e), byte(e) from an enum | The member's value, narrowed like an integer. |
Two conversions can fail, so they need ? or !!. Both fail with code 3:
| Call | Result |
|---|---|
E(n) for an enum E and an integer n | The member whose value is n; fails when there is none. |
T(value) for an object, interface or engine object type T | The 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
| Operands | Result |
|---|---|
byte with byte | int |
int32 with int32 or byte | int32, wrapping on overflow |
int with any integer | int |
float32 with float32 or byte | float32 |
float32 with int32 or int, or either operand float | float |
char with char, byte or int32 | int32 |
char with int | int |
char with float32 | float32 |
string + string, string + char, char + string | string |
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.
| Operands | Compares |
|---|---|
bool, byte, int, char, integer handles, enums, type | value |
float | value (NaN != NaN, 0.0 == -0.0) |
a number with a float | numeric value: 1 == 1.0 is true |
string | contents |
objects, interfaces, error, arrays, slices, maps, channels, tasks, engine objects | identity: whether both are the same object |
| function values | whether both are the same function |
anything with null | whether 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.