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.
| Include | APIs |
|---|---|
io | Printing and the standard input, output and error streams |
time | Timers, the monotonic clock and the UTC clock |
files | Files and directories |
sync | Mutex |
string | String methods, number conversion, string/buffer conversion |
math | Scalar math, including sqrt |
buffer | buffer and its methods |
socket | TCP sockets |
process | Child processes, process arguments, environment variables, paths, the platform query |
json | JSON documents |
regex | Regular 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.
| Functions | Contract |
|---|---|
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:
| Value | Family |
|---|---|
0 | Any supported family |
1 | IPv4 |
2 | IPv6 |
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 functions | Input |
|---|---|
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.