Skip to main content

Standard Library

Each file includes every package it uses. Standard packages use the same syntax as third-party ones: include "math"; makes sin, cos, and sqrt available. An included package's own dependencies stay private to it; using them directly needs another include.

IncludeAPIs
ioPrinting and the standard input, output and error streams
timeTimers, the monotonic clock and the UTC clock
filesFiles and directories
syncMutex
stringString methods, number conversion, string/buffer conversion
mathScalar math, including sqrt
bufferbuffer and its methods
socketTCP sockets
processChild processes, process arguments, environment variables, paths, the platform query
jsonJSON documents
regexRegular expressions

A function that reads a file as text and checks its length needs both files and string.

These names are reserved: include "io"; (or any other name in the table) always means the standard package, io/extra is an error, and your own modules can't be named io or io/... (see Packages).

This page explains how the packages fit together. Every declaration, with its exact signature and failure behaviour, is listed in the standard library reference.

Bytes travel in a buffer. Errors follow the normal BT rules: follow a fallible call with ?, handle it with !!, or branch it and observe the error at join. See Strings for string methods and number formatting, and Collections and buffers for the buffer API.

Math​

math provides the usual scalar math functions. None of them can fail, so they need no ?.

Arguments are float; integers and float32 values widen. Results are float, except the four classification functions, which return bool. When every float argument of a call is a float32 (literals adapt), the result is float32: the call computes in float and rounds once, so float32 c = cos(angle); needs no conversion. sqrt always returns float.

FunctionsContract
sin(x), cos(x), tan(x)Angles in radians
asin(x), acos(x), atan(x)Inverse trig; results in radians
atan2(y, x)Signed angle in radians, quadrant from both coordinates; y first
sinh(x), cosh(x), tanh(x)Hyperbolic functions
asinh(x), acosh(x), atanh(x)Inverse hyperbolic functions
exp(x), exp2(x), expm1(x)e^x, 2^x, and e^x − 1
log(x), log2(x), log10(x), log1p(x)Natural, base-2, and base-10 logs, and ln(1 + x)
pow(x, y), cbrt(x)Power; real cube root (negative x allowed)
hypot(x, y)Euclidean length without needless overflow or underflow
sqrt(x)Square root
floor(x), ceil(x), trunc(x)Round down, up, or toward zero; results stay floats
round(x)Nearest integer value; ties away from zero
fabs(x), copysign(x, y)Absolute value; magnitude of x with the sign of y
fmin(x, y), fmax(x, y)Minimum and maximum; one NaN operand returns the other
fmod(x, y)Remainder with the quotient truncated toward zero
remainder(x, y)Remainder with the nearest-integer quotient, ties to even
fma(x, y, z)x·y + z with one final rounding
isfinite(x), isinf(x), isnan(x), signbit(x)Classification and sign (including negative zero)

For fmin and fmax, two NaN operands select the second operand. Equal zero operands select negative zero for fmin and positive zero for fmax, regardless of argument order.

expm1 and log1p stay accurate near zero.

The circular trig functions (sin, cos, tan, asin, acos, atan, atan2) compute in single precision, so expect about seven significant digits. Every other function computes in double precision.

Out-of-range inputs and results never fail: acos(2.0) is NaN, log(0.0) is negative infinity, and a large exp(x) is positive infinity. Validate inputs, or test results with the classification functions. Results may differ in the last bits between platforms.

infallible fn heading(float dx, float dy) -> float {
return atan2(dy, dx);
}
infallible fn thrust_x(float radians, float force) -> float {
return cos(radians) * force;
}

Printing and standard streams​

From io:

infallible fn stdin() -> stream
infallible fn stdout() -> stream
infallible fn stderr() -> stream

infallible fn print(string text) -> void
infallible fn println(string text) -> void

stream.read(buffer buffer, int buffer_offset, int count) -> int
stream.read_within(buffer buffer, int buffer_offset, int count, float seconds) -> int
stream.write(buffer buffer, int buffer_offset, int count) -> int

print writes a string to standard output; println adds a newline. Both flush promptly and ignore output failures. Use stream.write when the program must handle an output error.

stream.read and stream.write transfer at most count bytes starting at buffer_offset. The range must fit in the buffer's current size, not merely its capacity, so size a destination buffer before reading into it. A transfer may move fewer bytes than requested. A read returning zero means end of input. Loop when a protocol needs an exact byte count.

stream.read_within is stream.read with a deadline. If the stream does not become readable within seconds, the read fails with code -331 instead of waiting forever. End of input is still a zero-byte success, so a caller can tell "too slow" from "closed". Zero seconds makes one non-blocking attempt. A negative, NaN, or infinite value is rejected. Regular files are always ready.

Stream transfers neither resize the buffer nor move its cursor. Track valid input with the returned count.

fn write_stdout(buffer data) -> void {
int offset = 0;
while (offset < data.size()) {
int moved = stdout().write(
data, offset, data.size() - offset
)?;
if (moved <= 0) {
throw new error(1, "stdout made no progress");
}
offset = offset + moved;
}
}

The process owns the standard streams. Do not close them.

Timers and clocks​

From time:

fn timer_after(float seconds) -> void
infallible fn timer_now() -> float
infallible fn monotonic_now() -> float
fn clock_utc_into(buffer output) -> int
infallible fn timeout(float seconds) -> channel<bool>
infallible fn deadline(float at) -> channel<bool>

timer_after completes after at least seconds. A negative duration is invalid. A plain call waits in the current execution; branch it to make the timer concurrent:

timer_after(0.25)?;

task<void> timer = branch timer_after(1.0);
string save = file_read_text("save.txt")?; // runs while the timer counts down
(join timer)?;

timer_now returns the current time, in seconds, of the clock that drives timer_after. In a standalone program that is real time since the program started; in a cTurtle game it is game time, which advances with each game step. Use differences between readings, not absolute values.

monotonic_now always reads a high-resolution real-time clock, in seconds, that never runs backwards. Use it to measure elapsed time regardless of frame or simulation time. Its starting point is unspecified; compare readings within one run of the program.

clock_utc_into writes the current UTC time as YYYY-MM-DDTHH:MM:SSZ into a pre-sized buffer and returns the byte count.

timeout returns a channel that is closed after at least seconds on the timer_after clock; deadline returns one that is closed once timer_now() reaches at. Use them as unless guards. A closed channel stays fired, so one timeout can bound several waits:

channel<bool> budget = timeout(2.0);
Mesh ship = (join loading_ship unless budget)?;
Job job = (jobs.receive() unless budget)?;

Each timeout runs until its time is up, so create one per deadline, not one per loop iteration. See Cancelling and timeouts.

Environment​

From process:

fn environment_get(string name) -> string

environment_get returns an environment variable as UTF-8, or an empty string when it is unset. The name must be nonempty and contain no =. Values over 128 KiB are rejected. There is no API to change the environment.

Mutex​

From sync:

Mutex lets one task at a time touch shared mutable state. While a task waits in lock(), other tasks keep running, and waiting tasks get the lock in the order they asked for it.

fn mutex_create() -> Mutex
fn Mutex.lock() -> void
infallible fn Mutex.unlock() -> void
fn Mutex.guard(MutexBody body) -> void

Pair lock with a deferred block so the unlock runs on every exit path, including a failure unwinding out of the function:

object Shared { int total; }

fn add_saved_score(Mutex lock, Shared shared) -> void {
lock.lock()? && { lock.unlock(); };
string text = file_read_text("score.txt")?; // the unlock still runs if this fails
shared.total = shared.total + int_from_string(text)?;
}

guard runs a MutexBody (any object with fn run() -> void) under the lock. It unlocks on success and on failure, and rethrows the body's error:

object AddOne {
Shared shared;
fn run() -> void { this.shared.total = this.shared.total + 1; }
}

lock.guard(AddOne { shared: shared })?;

The mutex is not reentrant: locking it twice on one task deadlocks that task. Prefer the designs in Concurrency, disjoint state or one owning task fed by a channel, and use a mutex when shared state is genuinely simpler.

Files​

From files:

fn file_open_read(string path) -> file
fn file_open_write(string path) -> file
fn file_read_bytes(string path) -> buffer
fn file_read_text(string path) -> string
fn file_write_text(string path, string text) -> void
fn file_write_text_atomic(string path, string text) -> void
fn file_remove(string path) -> void

file.read(buffer buffer, int offset, int count) -> int
file.write(buffer buffer, int offset, int count) -> int
file.size() -> int
infallible file.close() -> void

file_open_read opens an existing file. file_open_write creates the file if needed and truncates it. file_remove deletes a file.

file_read_bytes reads a whole file into a buffer and closes the file on success or failure. file_read_text does the same and returns the contents as a string; file_write_text writes a whole file and also closes it on success or failure. If another program is changing the file meanwhile, the result may mix old and new contents.

file.size returns the current byte size of a regular file, or -1 when the handle has no meaningful size (for example a pipe). It does not change the file offset. The file may change size before you read it. file_write_text_atomic writes <path>.tmp, closes it, and atomically replaces path. Use it for small state or configuration files that must never appear half-written after an interruption. Concurrent writers to one path still need application-level serialization.

File I/O is positional: offset is the byte offset in the file. Each transfer starts at index zero of the buffer, and count must fit in the buffer's current size. file.read returns zero at end of file. Reads and writes may be short, so advance the offset and repeat when an exact transfer matters. Transfers neither resize the buffer nor move its cursor.

Close a file exactly once, on every path, and do not use it or its aliases afterward. A deferred block does this:

fn read_chunk(string path, int file_offset, int count) -> buffer {
buffer data = new buffer(count);
file input = file_open_read(path)?;
{
int received = input.read(data, file_offset, count)?;
data.resize(received)?;
} && { input.close(); };
return data;
}

The block runs input.close() whether the read succeeds or fails. See Error handling for deferred blocks.

Directories​

Also from files:

object DirectoryEntryInfo {
bool isDirectory;
int size;
}

object FileEntry {
string name;
bool isDirectory;
int size;
}

fn directory_list(string path) -> array<FileEntry>
fn directory_open(string path) -> directory
directory.next(buffer name, DirectoryEntryInfo info) -> int
infallible directory.close() -> void
fn directory_make(string path) -> void
fn directory_temporary_into(string prefix, buffer output) -> int
fn directory_remove(string path) -> void
infallible fn directory_current() -> string
infallible fn directory_home() -> string

directory_list returns a snapshot of a directory's immediate children. It does not recurse or watch for changes. It closes the directory on success or failure. Directories sort before files. Names sort case-insensitively within each group, with a case-sensitive tie-break. Directories report size zero.

fn show_directory(string path) -> void {
array<FileEntry> entries = directory_list(path)?;
for (int i = 0; i < entries.length(); i++) {
FileEntry entry = entries[i];
if (entry.isDirectory) {
println("[dir] " + entry.name);
} else {
println(entry.name + " · " + int_to_string(entry.size) +
" bytes");
}
}
}

Use directory_open to consume entries without building the array. Size the name buffer before calling next. next returns the number of name bytes written, or zero at the end, and fills the DirectoryEntryInfo. A name that does not fit fails the call without consuming the entry. Close the directory once on every path.

directory_make creates one directory level. directory_temporary_into creates a new private directory, writes its path into a pre-sized buffer and returns the byte count. directory_remove removes an empty directory.

Relative file and directory paths resolve against the program's file root. directory_current returns that root as an absolute path; with no root set it is the process working directory. directory_home returns the user's home directory (HOME, or USERPROFILE on Windows), falling back to the current directory. Absolute paths stay absolute. These APIs do not confine paths to the project: .. components and symlinks may resolve outside it, and normal operating-system permissions govern access.

Child processes and paths​

process runs other programs and inspects paths. It is mostly useful for tools.

fn process_spawn(array<string> arguments, string cwd, array<string> environment) -> process
fn process_read_stdout(process child, buffer destination) -> int
fn process_read_stderr(process child, buffer destination) -> int
fn process_write_stdin(process child, buffer data) -> int
fn process_close_stdin(process child) -> void
fn process_poll(process child) -> int
fn process_close(process child) -> void
fn path_resolve_into(string path, buffer output) -> int
fn path_kind(string path) -> int
fn platform_windows() -> bool

arguments[0] names the executable. Arguments pass directly, with no shell. cwd sets the child's working directory. Each environment entry is NAME=value to set a variable or a bare NAME to remove one; later entries win. The parent's environment is unchanged.

Reads return the bytes available without waiting, so zero means either no bytes yet or end of output. Transfers may be partial and use the buffer's current size. Drain stdout and stderr while waiting so neither pipe fills. process_poll returns -2 while running, the exit status when finished, and -1 after close. process_close releases handles and terminates the owned process group or job, including descendants. Calling it twice is safe. A process is also closed when nothing refers to it any more, or when your program exits.

path_resolve_into writes the absolute path as UTF-8 into a pre-sized buffer and returns the byte count. path_kind returns 0 for missing, 1 for a file, 2 for a directory, and 5 for an executable file; Windows reports every file as 5.

The cturtle/tools/lib package adds deadlines, line protocols, argument parsing and scoped cleanup on top of these functions.

Process arguments​

Programs built or run with ctbt read their command-line arguments with:

infallible fn process_arg_count() -> int
fn process_arg(int index) -> string

Argument zero is conventionally the executable name or path. process_arg fails for an index outside the argument range.

fn print_arguments() -> void {
int index = 0;
while (index < process_arg_count()) {
println(process_arg(index)?);
index = index + 1;
}
}

JSON documents​

fn json_parse(string text) -> json

infallible json.has(string pointer) -> bool
infallible json.kind(string pointer) -> int
infallible json.count(string pointer) -> int
json.key(string pointer, int index) -> string
json.string(string pointer) -> string
json.bool_value(string pointer) -> bool
json.int_value(string pointer) -> int
json.float_value(string pointer) -> float
infallible json.close() -> void

Values are addressed by JSON Pointer: "" is the root, "/rules/0/match" a nested value, with ~0 and ~1 escaping ~ and /. kind returns 0 missing, 1 null, 2 bool, 3 number, 4 string, 5 array, or 6 object. count is the element or member count of an array or object, and 0 for a missing or scalar value. key returns an object's member name by index. A typed read fails when the value is missing or has another kind; int_value truncates.

json document = json_parse("{\"name\":\"BT\",\"tags\":[\"a\",\"b\"]}")?;
if (document.kind("/tags") == 5) {
string name = document.string("/name")?;
string first_tag = document.string("/tags/0")?;
}
document.close();

To decode a document straight into BT objects in a cTurtle game, see cTurtle JSON/YAML decoding.

Regular expressions​

fn regex_compile(string pattern) -> regex

infallible regex.find(string text, int start, array<int> starts, array<int> ends) -> int
infallible regex.capture_count() -> int
infallible regex.close() -> void

Patterns use Oniguruma's default (Ruby) syntax over UTF-8. find searches from byte offset start and returns the number of groups recorded, or zero when nothing matches. Group 0 is the whole match. Byte offsets go into starts[i] and ends[i]; size both arrays first, since find records no more groups than they hold. capture_count is the number of groups, including group 0.

regex word = regex_compile("\\bfn\\b")?;
array<int> starts = new array<int>(word.capture_count());
array<int> ends = new array<int>(word.capture_count());
if (word.find("call fn(value)", 0, starts, ends) > 0) {
println("match at " + int_to_string(starts[0]));
}
word.close();

TCP sockets​

socket supports TCP on Linux and Windows.

fn socket_connect_tcp(string host, string service, int family) -> socket
fn socket_listen_tcp(string host, string service, int family, int backlog) -> socket

socket.accept() -> socket
socket.receive(buffer buffer, int offset, int count) -> int
socket.send(buffer buffer, int offset, int count) -> int
socket.set_read_deadline(float seconds) -> void
infallible socket.clear_read_deadline() -> void
infallible socket.shutdown_read() -> int
infallible socket.shutdown_write() -> int
infallible socket.shutdown() -> int
infallible socket.close() -> void

service is normally a decimal port such as "8080". family selects address resolution:

ValueFamily
0Any supported family
1IPv4
2IPv6

An empty listen host ("") binds a local wildcard address. Use an explicit address such as "127.0.0.1" to accept only local IPv4 connections.

accept waits for and returns one connected peer. receive and send transfer bytes in the buffer range starting at offset; the range must fit in the buffer's current size. Either may move fewer bytes than requested. A receive returning zero means the peer ended its outgoing stream. Transfers neither resize the buffer nor move its cursor; call resize or seek when the next stage needs a different size or cursor.

The shutdown methods end one direction (or both) and return zero on success or a negative platform error code. close releases the socket. Close every listener and connected socket once, and do not use it or its aliases afterward.

fn send_all(socket socket, buffer data, int offset, int count) -> int {
int total = 0;
while (total < count) {
int moved = socket.send(data, offset + total, count - total)?;
if (moved <= 0) {
throw new error(1, "socket made no progress");
}
total = total + moved;
}
return total;
}

fn send_then_close(socket peer, buffer response) -> void {
{ send_all(peer, response, 0, response.size())?; }
&& { peer.close(); };
}

Read deadlines​

By default receive and accept wait with no time limit, so a peer that opens a connection and then withholds bytes can pin a server task forever (a slowloris attack). set_read_deadline(seconds) sets a deadline seconds from now. Every later receive and accept on that socket must finish before it, or fail with code -344. That timeout is distinct from end of input (still a zero-byte receive) and from transport errors, so a server can answer a slow client (for example with 408 Request Timeout) and still close on a real error:

peer.set_read_deadline(15.0)?;
int moved = 0;
{
moved = peer.receive(request, 0, request.size())?;
} !! {
if (err.code == -344) {
buffer reply = buffer_from_string("HTTP/1.1 408 Request Timeout\r\n\r\n")?;
peer.send(reply, 0, reply.size())?;
}
throw err;
};

The deadline is absolute, fixed when set, so it bounds a whole phase, such as an HTTP request's line and headers, rather than each read. Call set_read_deadline again to start a new phase, and clear_read_deadline to remove the bound for an unbounded body transfer or an idle keep-alive. Zero seconds makes the next read a non-blocking poll. A negative, NaN, or infinite value is rejected. A timed-out socket stays valid and closable.

Minimal TCP server​

fn serve_once() -> void {
socket listener = socket_listen_tcp("127.0.0.1", "8080", 1, 16)?;
socket peer = listener.accept()?;

buffer request = new buffer(4096);
int received = peer.receive(request, 0, 4096)?;

buffer response = make_response()?;

send_all(peer, response, 0, response.size())?;
peer.shutdown_write();
peer.close();
listener.close();
}

fn make_response() -> buffer {
buffer response = buffer_with_capacity(128)?;
response.append_string("HTTP/1.1 200 OK\r\n")?;
response.append_string("Content-Length: 13\r\n")?;
response.append_string("Connection: close\r\n")?;
response.append_string("\r\n")?;
response.append_string("Hello, World!")?;
return response;
}

Buffer appends are the right tool for piecewise text assembly. TCP is a byte stream: protocol code must accumulate and parse bytes by the protocol's framing rules, never assume one receive is one message.

cTurtle packages​

cTurtle adds packages of its own beside the standard library; the cTurtle API reference lists them. They follow the same call, error, object, task and nullability rules.

cTurtle JSON/YAML decoding​

cTurtle games decode JSON and YAML directly into ordinary BT objects. These functions come from include "cturtle/yaml";, so they work in games but not in standalone ctbt programs.

include "cturtle/yaml";

object Settings {
string name;
float speed;
array<string> tags;
}

fn load_settings(string asset_name) -> Settings {
Settings settings = Settings {
name: "unnamed", speed: 10.0, tags: new array<string>()
};
yaml_decode_asset(asset_name, settings)?;
return settings;
}

fn read_settings_text() -> Settings {
Settings settings = Settings {
name: "unnamed", speed: 10.0, tags: new array<string>()
};
json_decode("{\"name\":\"Scout\",\"speed\":25.0}", settings)?;
yaml_decode("speed: 30.0\n", settings)?;
return settings; // name: Scout, speed: 30, tags: empty
}

Each function returns void and fills the supplied target on success:

JSON / YAML functionsInput
json_decode(text, target) / yaml_decode(text, target)Document text
json_decode_asset(name, target) / yaml_decode_asset(name, target)A registered asset name, not a file path
json_decode_defaults(text, target, defaults) / yaml_decode_defaults(text, target, defaults)Text plus an array<DataObject> of prototype defaults
json_decode_asset_defaults(name, target, defaults) / yaml_decode_asset_defaults(name, target, defaults)Asset name plus prototype defaults

Keys match field names exactly. Supported fields are bool, byte, int, float, string, nested objects, and arrays of these. Missing fields keep their values, unknown keys are ignored, and supplied arrays replace old ones. New nested objects are zero-filled unless a matching prototype is in defaults. Ordinary BT objects satisfy DataObject.

Invalid data or type errors use code -390. A missing asset, or a file that cannot be opened, uses -391. A failure leaves the target unchanged. Do not change the target or prototypes during a decode.

The cturtle/yaml reference lists every decoding function. The YAML reader supports a subset of YAML, not all of it.