Collections and Buffers
BT has these built-in collection and byte-storage types:
array<T>: a growable indexed sequence.slice<T>: a view over an array or buffer that can grow withpush.map<K, V>: key-value lookup.channel<T>: communication between concurrent tasks.buffer: growable byte storage for parsing and I/O.
All are reference types. Assignment and parameter passing create aliases; they never copy the collection.
Arrays
Create an array with a literal or new:
object Player { int health; }
array<int> scores = [10, 20, 30];
array<int> empty = new array<int>();
array<int> zeros = new array<int>(100);
array<Player?> slots = new array<Player?>(4);
new array<T>(length) creates length zero-valued elements. The length must
not be negative. New reference elements are null, so the element type must
be nullable when T is an object or collection type: new array<Player>(4)
is a compile error. Scalars and strings need no ?.
A literal takes its element type from the destination it flows into: in
array<float> weights = [1, 2]; both elements convert to float, and
array<byte> raw = [1, 300]; stores 1 and 44. Without a destination type,
the first element decides, widening to float if a later element is a
float. An empty literal [] cannot infer its type; use new array<T>().
See Array literals.
Indexing
Indexes start at zero. Reading or writing outside the current length traps.
int first = scores[0];
scores[1] = 25;
Array methods
Array methods take positional arguments. Like functions, methods can fail
unless marked infallible; handle a failure with ? or !!.
| Method | Result | Description |
|---|---|---|
length() | infallible int | Number of elements |
capacity() | infallible int | Elements that fit before the next growth |
reserve(capacity) | infallible void | Ensure at least this capacity |
resize(length) | infallible void | Change the length; new elements are zero values |
fill(value) | infallible void | Replace every existing element; keep length and capacity |
push(value) | infallible void | Append an element |
pop() | T | Remove and return the last element |
insert(index, value) | void | Insert before index; length() appends |
remove(index) | T | Remove and return an element |
clear() | infallible void | Remove every element; keep the capacity |
pop fails on an empty array. insert and remove fail on an invalid index.
A negative reserve or resize is invalid. Like new array<T>(length),
resize requires a nullable element type for object and collection elements.
array_fill(array, value) does the same as array.fill(value). The value is
converted to the element type as for an indexed write. Filling an object array
stores the same object in every element, not copies of it.
fn edit_scores(array<int> scores) -> int {
scores.push(40);
scores.insert(1, 15)?;
scores.remove(0)?;
return scores.pop()?;
}
Iterating
int total = 0;
for (int score in scores) {
total = total + score;
}
Name two variables to get each element's index as well:
for (int i, int score in scores) {
ranked[i] = score;
}
Don't add or remove elements while iterating. Assigning a scalar loop variable does not replace the element, and assigning the index variable does not change which element comes next. Changing fields of an object element changes the referenced object. The same syntax loops over the entities a system works on; see Component views.
Slices and bulk copy
A slice is a view of a range of an array or buffer. Writing through a slice changes the original. Bounds are half-open: the low bound is included and the high bound is excluded. Either bound may be omitted.
array<int> values = [10, 20, 30, 40, 50];
slice<int> middle = values[1:4]; // 20, 30, 40
slice<int> prefix = values[:2]; // 10, 20
slice<int> tail = values[3:]; // 40, 50
slice<int> all = values[:];
middle[1] = 7; // values[2] is now 7
Slicing an array or buffer needs 0 <= low <= high <= length; invalid bounds
trap. A slice can be indexed, sliced again, iterated with for, passed and
returned.
Slice capacity and growth
A slice's capacity is the room from its first element to the end of the
storage behind it. Reslicing a slice may extend past its length up to that
capacity, so 0 <= low <= high <= capacity:
array<int> values = [10, 20, 30, 40, 50];
slice<int> s = values[1:3]; // 20, 30; capacity 4
slice<int> wider = s[0:4]; // 20, 30, 40, 50
| Method | Result | Behavior |
|---|---|---|
length() | int | Number of elements |
capacity() | int | Room before the slice must move |
push(value) | void | Append one element |
extend(sequence) | void | Append every element of an array or slice of the same element type |
push and extend follow Go's append. While the capacity allows, they
write into the storage behind the slice, overwriting whatever the array or
another slice holds there:
array<int> values = [1, 2, 3, 4];
slice<int> s = values[0:2];
s.push(9); // values[2] is now 9
When the capacity runs out, the slice moves to new storage about twice the size, copying its elements. A slice is a reference, so every variable and field holding that slice sees the move. Other slices over the old storage, and the array, keep the old storage and no longer share writes with it.
The => operator copies a whole array, slice, or buffer into another:
source => destination;
packet[0:8] => header;
input_buffer => output_buffer;
Range assignment is the destination-first spelling of the same copy:
destination[4:12] = source[0:8];
Source and destination must have the same element type and length; a length
mismatch traps. Arrays and slices mix freely. Buffers act as byte sequences,
so they copy to and from array<byte> and slice<byte>. Source and
destination may overlap, so you can shift elements within one array.
If the array grows past its capacity, it moves to new storage and the slice stays with the old one: writes through the slice no longer reach the array. Changes that don't grow the array, such as shifting, clearing or shrinking, stay visible through the slice.
Maps
Create an empty map with new, or a non-empty map with a literal:
map<string, int> visits = new map<string, int>();
map<string, int> ports = {
"http": 80,
"https": 443
};
An empty literal {} cannot infer its types; use new map<K, V>().
An indexed write inserts or overwrites. It is infallible:
ports["admin"] = 8080;
An indexed read is fallible. A miss fails with error code 1 and message
map key not found. Handle the read like any other fallible operation:
int port = ports["http"]?; // propagate a miss
ports["admin"] !! { ports["admin"] = 8080; }; // handle a miss locally (yields no value)
The ?? operator tests membership. It returns a bool and never fails:
if (ports ?? "http") {
int port = ports["http"]; // proven present: no '?' needed
}
Inside if (m ?? k), and inside if (m ?? k && ...), a read m[k] of that
key is infallible. BT has no map deletion, so a present key stays present.
The rule applies when m is a local variable or parameter and k is a local,
a parameter, or a literal. Reassigning m or k ends it. A present key whose
value is null (possible only for a V? value type) is a hit and returns
null.
Iterating and listing maps
for (string name in ports) { open_port(name); }
for (string name, int port in ports) { log_port(name, port); }
One variable gets each key; two get the key and the value. The order is
unspecified and can change as keys are added. Assigning to an existing key
during the loop is fine. Adding a new key during the loop traps (trap code
-11). Collect new keys and add them after the loop instead.
The loop keeps iterating the map it started with even if the body assigns the
variable it came from, and a null map is visited zero times.
| Method | Result | Behavior |
|---|---|---|
length() | int | Number of keys |
keys() | array<K> | A new array of the keys |
values() | array<V> | A new array of the values |
keys() and values() list entries in the loop's order, so keys()[i] and
values()[i] belong to the same entry while the map is unchanged.
Maps have no deletion.
Channels
A channel carries values of one type between concurrent tasks.
channel<int> events = new channel<int>(16);
The capacity controls buffering:
new channel<T>()ornew channel<T>(0)creates a rendezvous channel. A send and a receive meet directly.- A positive capacity lets that many values wait in the channel.
The capacity must not be negative.
Sending, receiving, and closing
events.send(42)?;
int event = events.receive()?;
events.close();
| Method | Result | Description |
|---|---|---|
send(value) | void | Send a value; waits when the channel is full |
receive() | T | Receive a value; waits when none is available |
close() | infallible void | Close the channel; safe to repeat |
Receivers drain values buffered before the close. After that, receive fails.
send on a closed channel fails. Both failures use code -300.
Channels and concurrent work
branch a send or receive when it should not wait in the current execution:
task<int> next = branch events.receive();
println("waiting for the next event");
int value = (join next)?;
For a long-lived producer or consumer, branch a function containing a loop:
fn produce(channel<int> output, int count) -> void {
int value = 0;
while (value < count) {
output.send(value)?;
value = value + 1;
}
output.close();
}
To wait on several channels at once, use select. See
Concurrency for select, tasks, and capture rules.
Buffers
buffer is growable byte storage with a parsing cursor. It is the standard
type for file, stream, and socket transfers. Include buffer for the
type and its methods, and string for the string conversions.
You never free a buffer yourself. close() does nothing.
Creating buffers
| Constructor or function | Description |
|---|---|
new buffer() | An empty buffer |
new buffer(size) | A buffer of size zero bytes |
buffer_with_capacity(capacity) | An empty buffer with reserved capacity |
buffer_from_string(text) | A buffer holding the string's bytes |
The new buffer forms can't fail, so they need no ?.
Sizes and capacities must not be negative.
buffer empty = new buffer();
buffer packet = new buffer(1024);
buffer output = buffer_with_capacity(4096)?;
buffer greeting = buffer_from_string("Hello, World!\n")?;
The size is the number of readable bytes. Capacity is reserved storage, not data.
Size and capacity
| Method | Result | Description |
|---|---|---|
size() | infallible int | Current byte count |
capacity() | infallible int | Reserved capacity |
reserve(capacity) | void | Ensure at least this capacity |
resize(size) | void | Change the byte count; growth adds zero bytes |
clear() | void | Set size and cursor to zero; keep the capacity |
Appending and indexed access
| Method | Result | Description |
|---|---|---|
append_string(text) | void | Append a string's bytes |
append_strings(first, second) | void | Append two strings in one operation |
append_int(value) | void | Append an integer in base 10 |
append_range(source, offset, count) | void | Append a byte range of another buffer |
push(byte) | void | Append one byte |
get(index) | int | Read one byte |
set(index, byte) | void | Replace an existing byte |
Byte values for push and set must be in 0..255. Indexes must lie within
the current size.
output.append_string("HTTP/1.1 200 OK\r\n")?;
output.push(13)?;
output.push(10)?;
int first = output.get(0)?;
String conversion
These methods come from string. Each fails if the range contains a
NUL byte.
| Method | Result | Description |
|---|---|---|
to_string() | string | Copy every byte into a new string |
range_to_string(offset, count) | string | Copy one range into a new string |
range_to_ascii_lower_string(offset, count) | string | Copy one range, ASCII-lowercased |
range_to_cached_string(offset, count, cached) | string | Return cached when it equals the range; otherwise a new copy |
string.to_buffer() converts the other way. See Strings.
Cursor operations
Each buffer has a cursor for sequential parsing.
| Method | Result | Description |
|---|---|---|
position() | infallible int | Cursor position |
remaining() | infallible int | Bytes from the cursor to the end |
seek(position) | void | Move the cursor |
read_byte() | int | Read one byte and advance; -1 at the end |
read(count) | buffer | Copy up to count bytes into a new buffer and advance |
The cursor ranges from zero through the size. read returns fewer than
count bytes when less data remains.
int checksum = 0;
packet.seek(0)?;
while (packet.remaining() > 0) {
int byte = packet.read_byte()?;
checksum = (checksum + byte) % 65536;
}
Slicing, searching, and comparison
A bracket range gives a slice<byte> view of the buffer's bytes, with the same
bounds rules as array slices. Writing through it changes the buffer:
slice<byte> header = packet[:12];
output[4:12] = packet[0:8];
Don't use a view while a file or socket operation is using the same buffer.
| Method | Result | Description |
|---|---|---|
slice(offset, count) | buffer | Copy an exact range into a new buffer; the cursor does not move |
copy_range(destination_offset, source, source_offset, count) | void | Copy into existing bytes; ranges may overlap |
find(text, start) | int | First match at or after start, or -1 |
find_range(text, offset, count) | int | First match wholly inside a range, or -1 |
find_byte(byte, offset, count) | int | First occurrence of a byte in a range, or -1 |
count_byte(byte, offset, count) | int | Occurrences of a byte in a range |
find_first_not_of(allowed, offset, count) | int | First byte not in allowed, or -1 |
find_last_not_of(allowed, offset, count) | int | Last byte not in allowed, or -1 |
range_has_only_text_bytes(offset, count, allow_tab) | bool | True when no byte is an ASCII control or DEL; tab optionally allowed |
range_matches(offset, count, text) | bool | Compare a range with a string |
range_ascii_case_matches(offset, count, text) | bool | Compare a range with a string, ignoring ASCII case |
matches_string(text) | bool | Compare the whole buffer with a string |
slice(offset, count) and read(count) return independent copies. Bracket
ranges return views. A range method given an out-of-range argument fails
without changing any byte.
fn request_target(buffer request) -> buffer {
int first_space = request.find(" ", 0)?;
int second_space = request.find(" ", first_space + 1)?;
if (first_space < 0 || second_space < 0) {
throw new error(400, "invalid request line");
}
return request.slice(
first_space + 1,
second_space - first_space - 1
)?;
}
Buffers used concurrently
Buffer methods don't lock. As with arrays and object fields, tasks that share a
buffer must coordinate with a mutex, a channel, or by joining; this includes
the parsing cursor and resize. See
Shared mutable state.
While a file or socket operation is using a buffer, wait for it to finish before reading the result, and don't change or resize the buffer. If a mutex guards a shared buffer, hold it until the operation finishes, not just until it starts. Cancelling an operation doesn't stop one that is already running, so don't reuse its buffer just because you cancelled it.