Skip to main content

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 with push.
  • 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 !!.

MethodResultDescription
length()infallible intNumber of elements
capacity()infallible intElements that fit before the next growth
reserve(capacity)infallible voidEnsure at least this capacity
resize(length)infallible voidChange the length; new elements are zero values
fill(value)infallible voidReplace every existing element; keep length and capacity
push(value)infallible voidAppend an element
pop()TRemove and return the last element
insert(index, value)voidInsert before index; length() appends
remove(index)TRemove and return an element
clear()infallible voidRemove 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
MethodResultBehavior
length()intNumber of elements
capacity()intRoom before the slice must move
push(value)voidAppend one element
extend(sequence)voidAppend 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.

MethodResultBehavior
length()intNumber 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>() or new 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();
MethodResultDescription
send(value)voidSend a value; waits when the channel is full
receive()TReceive a value; waits when none is available
close()infallible voidClose 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 functionDescription
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​

MethodResultDescription
size()infallible intCurrent byte count
capacity()infallible intReserved capacity
reserve(capacity)voidEnsure at least this capacity
resize(size)voidChange the byte count; growth adds zero bytes
clear()voidSet size and cursor to zero; keep the capacity

Appending and indexed access​

MethodResultDescription
append_string(text)voidAppend a string's bytes
append_strings(first, second)voidAppend two strings in one operation
append_int(value)voidAppend an integer in base 10
append_range(source, offset, count)voidAppend a byte range of another buffer
push(byte)voidAppend one byte
get(index)intRead one byte
set(index, byte)voidReplace 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.

MethodResultDescription
to_string()stringCopy every byte into a new string
range_to_string(offset, count)stringCopy one range into a new string
range_to_ascii_lower_string(offset, count)stringCopy one range, ASCII-lowercased
range_to_cached_string(offset, count, cached)stringReturn 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.

MethodResultDescription
position()infallible intCursor position
remaining()infallible intBytes from the cursor to the end
seek(position)voidMove the cursor
read_byte()intRead one byte and advance; -1 at the end
read(count)bufferCopy 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.

MethodResultDescription
slice(offset, count)bufferCopy an exact range into a new buffer; the cursor does not move
copy_range(destination_offset, source, source_offset, count)voidCopy into existing bytes; ranges may overlap
find(text, start)intFirst match at or after start, or -1
find_range(text, offset, count)intFirst match wholly inside a range, or -1
find_byte(byte, offset, count)intFirst occurrence of a byte in a range, or -1
count_byte(byte, offset, count)intOccurrences of a byte in a range
find_first_not_of(allowed, offset, count)intFirst byte not in allowed, or -1
find_last_not_of(allowed, offset, count)intLast byte not in allowed, or -1
range_has_only_text_bytes(offset, count, allow_tab)boolTrue when no byte is an ASCII control or DEL; tab optionally allowed
range_matches(offset, count, text)boolCompare a range with a string
range_ascii_case_matches(offset, count, text)boolCompare a range with a string, ignoring ASCII case
matches_string(text)boolCompare 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.