Standard Library

Standard Library

The Sun standard library is distributed as a precompiled stdlib.moon file. Include it in your manifest and import the std namespace:

using std;
 
function main() void {
    var allocator = make_heap_allocator();
    // Use stdlib types...
}
 
manifest {
    libraries: ["stdlib.moon"]
}

An installed compiler finds stdlib.moon on its own — the Homebrew and Debian packages put it where the compiler already looks. SUN_PATH is only needed when you built from source, where it points at the build/ directory: export SUN_PATH=/path/to/sun/build. See Path Resolution for every place that is searched.

The library's own tests are Sun code: each stdlib/<area>_tests.sun sits next to the file it tests, listed under test_files: in the manifest (see Testing). They compile only into the test binary, never into stdlib.moon.

HeapAllocator

The standard memory allocator using system malloc/free. Many stdlib types require an allocator for construction.

var allocator = make_heap_allocator();
MethodSignatureDescription
alloc<T>(count)(i64) raw_ptr<T>Allocate array of count elements
alloc_raw(size)(i64) raw_ptr<i8>Allocate size raw bytes
dealloc(ptr, size)(raw_ptr<i8>, i64) voidFree allocated memory
create<T>(args...)(...) raw_ptr<T>Allocate and construct a single T
copy()() HeapAllocatorCreate a copy of the allocator
var allocator = make_heap_allocator();
var p: raw_ptr<Point> = allocator.create<Point>(3, 4);
// p points to a heap-allocated, initialized Point
_free(p);  // Manual cleanup

Unique<T>

A smart pointer that automatically frees its data when deinit is called (at scope exit). Provides safe access to heap-allocated objects without manual unsafe blocks.

var allocator = make_heap_allocator();
var p = Unique<Point>(allocator.create<Point>(3, 4));
p.get().x;     // Safe access - returns ref T
p.get_raw();   // Get raw pointer for low-level operations
p.is_null();   // Check if null
// p.deinit() called automatically at scope exit
MethodSignatureDescription
get()() ref TSafe dereference - returns a reference to the value
get_raw()() raw_ptr<T>Get the underlying raw pointer
is_null()() boolCheck if pointer is null
deinit()() voidFree memory (called automatically)

Shared<T>

The shared-ownership counterpart to Unique<T>: Unique has one owner, Shared has as many as you like, and it is safe to hand to several threads. The value lives in a heap box next to a std.thread.Mutex and an atomic count of the live handles. clone() adds an owner, and the last handle to go drops the value and frees the box.

Since Sun never copies implicitly, every extra owner is a visible clone(). The value is reachable only through lock(), which waits for the lock and returns a guard that releases it when dropped.

var allocator = make_heap_allocator();
var s = Shared<Counter>(allocator, Counter());
 
var c = s.clone();        // two owners now
s.count();                // 2
 
if (true) {
    var g = s.lock();     // g holds the lock
    g.get().n = 42;       // the value, as a reference
}                         // g is dropped here, which unlocks
MethodSignatureDescription
clone()() Shared<T>Another owner of the same value
lock()() SharedGuard<T>Wait for the lock, return a guard that holds it
count()() i32How many owners there are right now
is_null()() boolWhether the handle has been moved out of
deinit()() voidDrop one owner (called automatically)
SharedGuard<T> methodSignatureDescription
get()() ref TThe shared value, as a reference
deinit()() voidRelease the lock (called automatically)

See Threads for sharing one value between several threads.

Vec<T>

A generic growable array backed by contiguous memory. Implements IIterable<T, Vec<T>> for use with for-in loops.

var allocator = make_heap_allocator();
var v = Vec<i32>(allocator, 8);  // Initial capacity 8
v.push(10);
v.push(20);
v.push(30);
var val = v.get(0);       // 10
v.set(1, 25);             // Update index 1
var last = v.pop();       // Option.Some(30) (removes last element)
var len = v.size();       // 2
MethodSignatureDescription
push(value)(T) voidAppend element (grows if needed)
pop()() Option<T>Remove and return last element; None when empty (ownership moves to the caller)
get(index)(i64) TGet element at index
set(index, value)(i64, T) voidSet element at index
size()() i64Current number of elements
capacity()() i64Current allocated capacity
is_empty()() boolCheck if empty
reserve(n)(i64) voidEnsure capacity for at least n elements
clear()() voidRemove all elements (doesn't deallocate)
first()() Option<T>Get first element; None when empty
last()() Option<T>Get last element; None when empty
iter()() VecIterator<T>Get iterator for for-in loops

Indexing

Vec<T> supports bracket indexing syntax:

v[0] = 42;       // Calls __setindex__
var x = v[0];    // Calls __index__

Iteration

var v = Vec<i32>(allocator, 4);
v.push(1); v.push(2); v.push(3);
 
for (var x: i32 in v) {
    println(x);
}

Map<K, V>

A generic hash map using open addressing with linear probing. Implements IIterable<V, Map<K, V>> for iterating over values. Supports i32, i64, and String keys.

var allocator = make_heap_allocator();
var m = Map<i64, i32>(allocator, 16);  // Initial capacity 16
m.insert(42, 100);
var fallback: i32 = 0;
var val = m.get_or_default(42, fallback);   // 100
ignore_error(m.remove(42));
MethodSignatureDescription
insert(key, value)(K, V) voidInsert or update a key-value pair
get(key)(K) LookupResult<ref V>Borrow the value or return NotFound
find(key)(K) Option<ref V>Get value by key; None if not present
get_or_default(key, default)(K, ref V) ref VGet value or return default
remove(key)(K) LookupResult<V>Remove the value or return NotFound
contains(key)(K) boolCheck if key exists
size()() i64Number of entries
capacity()() i64Number of buckets
is_empty()() boolCheck if empty
clear()() voidRemove all entries
iter()() MapIterator<K, V>Get iterator over values

Handle get and remove with match, or propagate their LookupResult with try from a function that returns a compatible result enum.

Iteration

var m = Map<i64, i32>(allocator, 16);
m.insert(1, 10);
m.insert(2, 20);
 
for (var v: i32 in m) {
    println(v);  // Iterates over values
}
⚠️

The map automatically grows when load factor exceeds 70%. Initial capacity should be chosen based on expected size for best performance.

String

A heap-allocated mutable string backed by Matrix<u8>. Supports slicing via the [start:end] syntax which returns a StringView.

var allocator = make_heap_allocator();
var s = String(allocator, "Hello");
var t = String("literal only");  // allocator-less: uses a fresh HeapAllocator
println(s.length());   // 5
var ch = s.at(0);          // b'H'
s.append_char(b'!');       // Append a byte
s.push('😀');              // Append a char, UTF-8 encoded

A String holds UTF-8 and is indexed by byte: at, length and the *_char methods all work in bytes, so they pair with byte literals (b'!'). The methods that speak in Unicode scalar values — push, iterate_chars, count_chars, and the char overloads of find / rfind / contains — take and yield char. See Characters and Bytes.

MethodSignatureDescription
length()() i64Current string length
capacity()() i64Allocated capacity
is_empty()() boolTrue when the string has no bytes
at(i)(i64) AccessResult<u8>Read a byte; reject indices outside length()
set_at(i, val)(i64, u8) AccessResult<void>Write a byte; reject indices outside length()
unsafe_at(i)(i64) u8Unchecked read; requires an unsafe block
unsafe_set_at(i, val)(i64, u8) voidUnchecked write; requires an unsafe block
append_char(ch)(u8) voidAppend a single byte
append(other)(const ref String) voidAppend another String
append_literal(s)(static_ptr<u8>) voidAppend a string literal
equals_literal(s)(static_ptr<u8>) boolCompare with a string literal
find_char(ch)(u8) Option<i64>Index of first occurrence; None if absent
rfind_char(ch)(u8) Option<i64>Index of last occurrence; None if absent
contains_char(ch)(u8) boolCheck if the byte occurs
push(c)(char) voidAppend the UTF-8 encoding of a char (1 to 4 bytes)
iterate_chars()() CharsIterate the string as Unicode scalar values
count_chars()() i64Number of scalar values, which is length() only for ASCII
find(c)(char) Option<i64>Byte index of the first occurrence of a char; None if absent
rfind(c)(char) Option<i64>Byte index of the last occurrence of a char; None if absent
contains(c)(char) boolCheck if the char occurs
find(needle)(static_ptr<u8>) Option<i64>, (const ref String) Option<i64>Index of first occurrence of a substring; None if absent
contains(needle)(static_ptr<u8>) bool, (const ref String) boolCheck if the substring occurs
starts_with(s) / ends_with(s)(static_ptr<u8>) bool, (const ref String) boolPrefix / suffix check
append_i64(n) / append_u64(n)(i64) voidAppend an integer in decimal
append_f64(x)(f64) voidAppend a float in its shortest round-trip form (0.1, 1e+21)
append(x)(f64) void, (f32) void, ...Overloads used by ${x} interpolation for every primitive
parse_i64()() Option<i64>Whole string as a decimal integer; None if malformed or out of range
parse_f64()() Option<f64>Whole string as a decimal float; None if malformed
bytes()() raw_ptr<u8> (const method)Borrow the bytes without allocating or adding a NUL terminator
c_str()() raw_ptr<u8>NUL-terminated pointer for C, valid until the next mutation

bytes() works on a const ref String. Read only length() bytes and do not write through the pointer. The pointer remains valid until the next mutation or until the string is destroyed. Pass it with the length to C functions that accept a byte buffer.

The terminator c_str() writes sits past the length, so the string itself is unchanged. from_c_str(alloc, p) is the other direction — it copies a NUL-terminated C string into an owned String, which is how Env and read_dir keep text returned by libc.

var path = String(alloc, "/tmp/");
path.append_literal("out.txt");
write_string(path.c_str(), body);

Text Manipulation

Methods that keep the same bytes, or fewer, edit the string in place. Methods that produce new strings take the allocator to build them with.

var line = String(allocator, "  name = Ada  ");
line.trim();                              // "name = Ada"
line.replace(" = ", "=");                 // "name=Ada"
line.to_upper();                          // "NAME=ADA"
 
var fields = line.split(allocator, 61);   // ['=' is 61] -> ["NAME", "ADA"]
var again = join(allocator, fields, "=");  // "NAME=ADA"
MethodSignatureDescription
to_lower() / to_upper()() voidConvert ASCII letters in place
trim()() voidDrop leading and trailing ASCII whitespace in place
trim_start() / trim_end()() voidDrop whitespace from one end in place
reverse()() voidReverse the bytes in place
replace(from, to)(static_ptr<u8>, static_ptr<u8>) void, (const ref String, const ref String) voidReplace every occurrence in place; an empty from does nothing
clone(alloc)(const ref HeapAllocator) StringCopy into a new String
substr(alloc, start, count)(const ref HeapAllocator, i64, i64) AccessResult<String>Copy a byte range or return OutOfBounds
split(alloc, sep)(const ref HeapAllocator, u8) Vec<String>, (const ref HeapAllocator, static_ptr<u8>) Vec<String>Split into owned pieces; always yields one piece more than there are separators
split_nonempty(alloc, sep)(const ref HeapAllocator, u8) Vec<String>, (const ref HeapAllocator, static_ptr<u8>) Vec<String>Like split, but empty pieces are dropped: adjacent, leading, and trailing separators yield nothing
split_whitespace(alloc)(const ref HeapAllocator) Vec<String>Split on runs of ASCII whitespace; never yields an empty piece
join(alloc, parts, sep)(const ref HeapAllocator, const ref Vec<String>, static_ptr<u8>) StringFree function: concatenate pieces with a separator between them

Slicing

String slicing returns a StringView (non-owning view, no copy):

var s = String(allocator, "Hello, World!");
var view = s[0:5];   // StringView of "Hello"
println(view.length());  // 5

Character helpers

Free functions in std for moving between char and code points.

FunctionSignatureDescription
char_of(code)(i64) Option<char>A char from a code point; None for a surrogate or anything above U+10FFFF
code_of(c)(char) i64The code point of a char
compute_utf8_len(code)(i64) i64Bytes the UTF-8 encoding of a code point occupies
encode_utf8_byte(code, j)(i64, i64) u8Byte j of that encoding

char_of is the checked counterpart to _convert<char>(n), which does not verify that n is a Unicode scalar value.

Chars

Iterates a String as Unicode scalar values, decoding UTF-8 as it goes. Obtained from String.iterate_chars().

var s = String(alloc, "héllo😀");
s.length();       // 10 bytes
s.count_chars();   // 6 scalar values
 
var it = s.iterate_chars();
for (var c: char in it) {
    print(c);
}
MethodSignatureDescription
next(self)(ref Chars) Option<char>Next scalar value; None at the end
offset()() i64Byte offset of the scalar value next will return

Bytes that are not well-formed UTF-8 decode as U+FFFD and advance one byte, so iteration always terminates and always covers the whole string.

A Chars borrows the string's bytes: it must not outlive the String, and any mutation of that String invalidates it.

StringView

A non-owning view into a String. Provides read-only access to a substring without copying.

MethodSignatureDescription
length()() i64View length
at(i)(i64) AccessResult<u8>Read a byte; reject indices outside the view
unsafe_at(i)(i64) u8Unchecked read relative to the view; requires an unsafe block
equals_literal(s)(static_ptr<u8>) boolCompare with a string literal
find_char(ch)(u8) Option<i64>Index of first occurrence; None if absent
rfind_char(ch)(u8) Option<i64>Index of last occurrence; None if absent
contains_char(ch)(u8) boolCheck if the byte occurs
append_i64(n) / append_u64(n)(i64) voidAppend an integer in decimal
append_f64(x)(f64) voidAppend a float in its shortest round-trip form (0.1, 1e+21)
append(x)(f64) void, (f32) void, ...Overloads used by ${x} interpolation for every primitive
parse_i64()() Option<i64>Whole string as a decimal integer; None if malformed or out of range
parse_f64()() Option<f64>Whole string as a decimal float; None if malformed
⚠️

A StringView must not outlive the parent String. The view holds a reference to the String's internal memory.

Matrix<T>

N-dimensional matrices backed by contiguous heap memory. Supports bracket indexing and slicing.

var allocator = make_heap_allocator();
var m = Matrix<i32>(allocator, [3, 3]);   // 3x3 matrix
m[0, 0] = 1;
m[1, 1] = 5;
var val = m[1, 1];  // 5
MethodSignatureDescription
get(indices)(ref array<i64>) TGet element at indices
set(indices, value)(ref array<i64>, T) voidSet element at indices
dim(i)(i64) i64Size of dimension i
size()() i64Total number of elements
ndims()() i64Number of dimensions

See Matrices for detailed documentation.

Threads

stdlib/thread.sun (module std.thread). spawn starts an OS thread and hands back a handle that joins when it is dropped, so a thread never outlives the scope that started it.

using std.thread;
 
var t = spawn((n: i32) => i32 { return n * 2; }, 21);
var answer = t.join();   // 42
FunctionSignatureDescription
spawn()<F: _Lambda>(fn: F, args...: _params_of<F>) Thread<_return_type_of<F>>Run fn on a new thread with args, which move into it
Thread<T> methodSignatureDescription
join()() TBlock until the thread finishes, and take its result

Mutex

Mutual exclusion, using an atomic compare-and-swap for the uncontended case and futex blocking under contention.

var m = Mutex();
m.lock();
// Critical section
m.unlock();
MethodSignatureDescription
lock()() voidAcquire the lock (blocks if contended)
unlock()() voidRelease the lock

See Threads for the whole picture.

File I/O

stdlib/io.sun (module std.io). Every path is taken two ways: as a static_ptr<u8> — what a string literal is — and as a ref String for a path put together at runtime. Fallible calls return SystemResult<T> with an owned Error. The examples use try inside a function returning a compatible result.

using std;
using std.io;
 
var f = File();
try f.open("notes.txt", FileMode.Write);
f.write("Hello, World!");
f.close();
 
// Or in one step
var text = try read_to_string(alloc, "notes.txt");
 
// A runtime path
var path = String(alloc, "/tmp/");
path.append_literal("report.csv");
try write_string(path, text);

FileMode is Read (must exist), Write (create or truncate) or Append. Whence is Start, Current or End.

MethodSignatureDescription
open(path: Path, mode: FileMode) SystemResult<void>Open a file
adopt(fd: i32) voidTake ownership of an open descriptor
write(data: static_ptr<u8>) i64 / (data: ref String) i64Bytes written
write_bytes(buf: raw_ptr<u8>, len: i64) i64Write raw bytes
read_all(alloc: ref HeapAllocator) SystemResult<String>Read to end of file
read_into(buf: raw_ptr<u8>, len: i64) i64Read into a buffer
seek(offset: i64, whence: Whence) i64New offset from the start
tell / size() i64Current position / total length
sync / truncate() SystemResult<void> / (length: i64) SystemResult<void>Flush / resize
close / is_open / get_fdClose (idempotent), query, borrow the fd

A File closes itself at scope exit. The descriptor it holds is 0 when it holds nothing, so a moved-from File — which is zeroed — closes nothing rather than closing stdin.

Free functions:

FunctionSignature
read_to_string(alloc: ref HeapAllocator, path: Path) SystemResult<String>
write_string(path: Path, contents: ref String) SystemResult<void>
remove_file / remove_dir(path: Path) SystemResult<void>
rename_file(old_path: Path, new_path: Path) SystemResult<void>
make_dir(path: Path, mode: i32) SystemResult<void>
exists / is_dir / is_file(path: Path) bool
get_file_size(path: Path) SystemResult<i64>
read_line(alloc: ref HeapAllocator) Option<String>

Path stands for the two overloads each of these has: static_ptr<u8> (a string literal) and ref String (a path built at runtime). rename_file has all four combinations.

read_line reads stdin one byte at a time and strips the newline, returning None only at end of input with nothing read. It is deliberately unbuffered: a buffered reader would swallow bytes a child process spawned later expects to find on the descriptor.

There is no stat. struct stat has a different field order and padding on x86-64 and aarch64, and the stdlib is compiled from one source for both, so the questions people actually ask are answered with access, opendir and lseek instead.

Reading a directory

read_dir returns the entries with their kind attached, taken from the directory record itself — no second syscall per entry, and no application-level parsing of getdents64 output.

var entries = try read_dir(alloc, "/etc");
for (var e: DirEntry in entries) {
    if (e.is_dir()) {
        print("dir  ");
    } else {
        print("file ");
    }
    println(e.name());
}
ItemSignatureDescription
read_dir(alloc: ref HeapAllocator, path: Path) SystemResult<Vec<DirEntry>>. and .. excluded, filesystem order
DirEntry.name() ref StringBorrowed — the entry keeps it
DirEntry.kind() FileKindFile, Dir, Symlink, Other or Unknown
DirEntry.is_dir / is_file() bool

Some filesystems do not report a kind, giving FileKind.Unknown; is_dir(path) answers it with a syscall when that matters.

Waiting on descriptors

EventLoop uses epoll on Linux and kqueue on macOS. It watches descriptors without scanning every registration on each wait. Call open() before adding borrowed descriptors, and remove them before closing or reusing them. The loop owns its queue and closes it on destruction. Registration changes can allocate through the supplied allocator; waits reuse storage reserved by the constructor.

var loop = EventLoop(alloc, 64); // maximum native events per batch
try loop.open();
try loop.add(sock_fd, POLLIN, 42u64);
var count = try loop.wait(std.time.create_duration_millis(250));
for (var i: i64 = 0; i < count; i = i + 1) {
    var event = try loop.event_at(i);
    if (event.is_readable()) { /* read from event.fd(); identify it by event.token() */ }
    if (event.is_hup()) { /* drain buffered input before closing */ }
}
try loop.modify(sock_fd, POLLIN | POLLOUT, 42u64);
try loop.remove(sock_fd);

Interest accepts POLLIN, POLLOUT, both, or zero to suspend normal readiness. Errors and hangups are reported through is_error() and is_hup(); their reporting for suspended descriptors depends on the OS. Tokens are full-width u64 values and need not be unique. Duplicate adds and missing modify/remove operations return errors. Failed changes preserve registrations; if macOS cannot roll back a partial filter change, the loop closes and reports that failure.

Readiness is level-triggered: data remains ready until consumed. An integer wait timeout of zero polls, -1 waits indefinitely, and other negative values are rejected. Duration waits round up fractional milliseconds. Signals return an interrupted system error. Each wait replaces the previous batch, including on failure. event_at() checks bounds and returns an independent snapshot without allocating. macOS may return separate read and write events for one descriptor; event order is unspecified. A full batch does not discard remaining readiness. Use a loop from one thread at a time.

Poller wraps portable poll(2). Removal preserves the remaining registration order and readiness flags in linear time. Its integer timeout overload keeps descriptors in registration order, so is_readable(i) answers for the i-th one. Nothing here needs a struct pollfd assembled by hand.

var poller = Poller(alloc);
poller.add_read(sock_fd);
poller.add_read(pipe_fd);
 
var ready = try poller.wait(1000);   // -1 blocks forever
if (poller.is_readable(0)) { /* sock_fd has data */ }
if (poller.is_hup(1)) { /* pipe_fd's writer closed */ }

For a finite wait, pass a Duration to receive only the descriptors that reported an event. Positive fractions of a millisecond round up, and very long durations clamp to the largest i32 timeout.

using std.time;
 
var events = try poller.wait(create_duration_millis(250));
for (var event: ref Event in events) {
    if (event.is_readable()) { /* event.fd() has data */ }
    if (event.is_hup()) { /* event.fd() reached end of stream */ }
}
 
poller.modify(sock_fd, POLLOUT); // replaces interest in every matching registration
poller.remove(sock_fd);       // removes every registration for sock_fd
MethodSignatureDescription
add_read / add_write / add_read_write(fd: i32) voidWatch a descriptor
add(fd: i32, events: i16) voidWatch with an explicit POLL* mask
modify(fd: i32, events: i16) boolReplace interest and clear readiness for every matching registration
remove(fd: i32) boolRemove every matching registration; report whether one existed
wait(timeout_ms: i32) SystemResult<i32>Number ready; -1 blocks
wait(timeout: const ref Duration) SystemResult<Vec<Event>>Ready descriptors in registration order
is_readable / is_writable / is_hup / is_error(i: i64) bool
fd_at / revents(i: i64) i32 / (i: i64) i16
clear / countReset, or how many are watched

The bits are POLLIN, POLLPRI, POLLOUT, POLLERR, POLLHUP and POLLNVAL. Do not call add while a wait is in flight: growing the set reallocates it.

An Event exposes fd() and the raw mask through events(). Its is_readable(), is_priority(), is_writable(), is_error(), is_hup() and is_invalid() helpers test the corresponding bit without exposing platform details.

Terminal

stdlib/terminal.sun (module std.terminal). Scoped raw and cbreak modes, terminal size queries and single-key reads with a timeout, on Linux and macOS. Nothing here needs termios or ioctl bound by hand: the struct mirror and the per-OS flag values live in target_linux.sun / target_darwin.sun.

Inside a function returning a compatible result, use try to propagate failures:

using std.io;
using std.terminal;
using std.time;
 
if (is_terminal(STDIN_FD)) {
    var mode = try TerminalMode(STDIN_FD, Mode.Raw, SignalPolicy.AsInput);
    var size = try get_size(STDOUT_FD);
    while (true) {
        match try read_key(STDIN_FD, create_duration_millis(250)) {
            Option.Some(key) => { if (key == 3 or key == b'q') { break; } },
            Option.None => { size = try get_size(STDOUT_FD); }   // maybe resized
        };
    }
}   // `mode` leaves scope here and the terminal is restored

TerminalMode saves the descriptor's settings when it is constructed and puts them back when it leaves scope, whether by reaching the end of the block, by return, or by try propagating an error. Call restore() to put them back early; a second call does nothing. A moved-from guard is all zeros and restores nothing, so handing a guard to another owner moves the duty with it. Constructing one on a descriptor that is not a terminal returns an error with ENOTTY and changes nothing.

ModeClearsKeeps
Rawecho, line editing, CR/NL translation, Ctrl-S/Ctrl-Q flow control, output processing
Cbreakecho, line editingoutput processing and CR/NL translation

In Raw mode a "\n" no longer returns to column 0; write "\r\n". Both modes set VMIN=1, VTIME=0, so a read returns as soon as one byte arrives, and both the change and the restore use TCSANOW, so input typed around the switch is never discarded.

Signal policy

The library never installs a signal handler. Sun cannot make a Sun function a C signal handler, and a process-wide handler installed by a library would be a surprise anyway, so what the interrupt keys do is a choice the caller makes once, when constructing the guard:

SignalPolicyMeaning
PassthroughISIG stays on. Ctrl-C, Ctrl-\ and Ctrl-Z raise SIGINT, SIGQUIT and SIGTSTP for the foreground process group and never arrive as bytes.
AsInputISIG is off. The same keys arrive as bytes 3, 28 and 26, and the program decides what they mean.

A process killed by a signal runs no destructors, so under Passthrough a Ctrl-C leaves the terminal in raw mode; the shell's reset or stty sane repairs it. To avoid that, use AsInput and exit normally when byte 3 arrives, as the example above does. Cbreak with Passthrough is the gentler combination when signals must keep working: output processing stays on, so a terminal left behind is still usable.

SIGWINCH is not used. A resize is observed by calling get_size again, for example whenever read_key returns None.

FunctionSignatureDescription
is_terminal(fd: i32) boolisatty; false for pipes, files and invalid descriptors
get_size(fd: i32) SystemResult<Size>rows and cols; returns ENOTTY on a non-terminal
read_key(fd: i32, timeout: const ref Duration) SystemResult<Option<u8>>One byte, None on timeout; returns an error once input has closed
try_read_key(fd: i32) SystemResult<Option<u8>>read_key with a zero timeout
read_attributes(fd: i32) SystemResult<Termios>Snapshot of the line-discipline settings
has_echo / is_canonical / delivers_signals(const ref Termios) boolRead the ECHO / ICANON / ISIG bits of a snapshot
TerminalModeSignatureDescription
init(fd: i32, mode: Mode, signals: SignalPolicy) _Result<void, Error>Save and switch
restore() SystemResult<void>Put the saved settings back now; idempotent
is_active() boolWhether the terminal is still in the applied mode

read_key allocates nothing and never touches a descriptor the caller did not pass, so it is safe to use on stdin alongside a child process. A multi-byte key (an arrow key is ESC [ A) arrives as several calls: after a 27, drain the rest with try_read_key. Termios is opaque; equals(const ref Termios) compares two snapshots. The native request codes TIOCGWINSZ, TIOCSWINSZ and TIOCSCTTY are public for programs that drive ioctl themselves.

Terminal operations return SystemResult<T>. TerminalMode construction still uses SystemResult<TerminalMode>; try can propagate either result.

Environment

Env in module std.env owns a snapshot of the process environment. It copies entries in native order, preserves duplicate names, splits each entry at its first =, and is directly iterable as borrowed EnvEntry values.

The fallible calls below use try inside a function returning SystemResult<T> or another compatible result enum.

using std;
using std.env;
 
var environment = Env(alloc);
match environment.get("HOME") {
    Option.Some(value) => { println(value); },
    Option.None => { }
};
 
for (var entry: ref EnvEntry in environment) {
    println(entry.name());
}
 
try environment.set("LANG", "C");
var here = try environment.cwd();
MethodSignatureDescription
get(name: Name) Option<ref String>First matching entry, or None
has(name: Name) boolWhether the snapshot contains the name
set(name: Name, value: Name) SystemResult<void>Mutate libc, then refresh
remove(name: Name) SystemResult<void>Mutate libc, then refresh
refresh() voidObserve changes made outside this Env
size / is_empty() i64 / () boolSnapshot size
iter() EnvIteratorBorrow entries in native order
cwd() SystemResult<String>Working directory
set_cwd(path: Name) SystemResult<void>Change it process-wide
args(argc, argv) Vec<String>Copy argv, with argv[0] first

Name means either static_ptr<u8> or ref String; set provides all four combinations. A snapshot is not automatically live: external changes appear after refresh(), while its own set and remove refresh automatically.

function main(argc: i32, argv: raw_ptr<raw_ptr<i8>>) i32 {
    var alloc = make_heap_allocator();
    var environment = Env(alloc);
    var cli = environment.args(argc, argv);
    for (var s: ref String in cli) { println(s); }
    return 0;
}

Processes

stdlib/process.sun (module std.process). Command covers running a program; the raw POSIX calls are there for programs that drive the fork themselves. Fallible operations return SystemResult<T> with an owned Error; try propagates its code and message to a compatible caller result.

using std.process;
 
var cmd = Command(alloc, "/bin/sh");
cmd.add_arg("-c");
cmd.add_arg("printf hello");
var out = try cmd.output();
 
println(out.status());     // 0
println(out.stdout());     // hello

Command

MethodSignatureDescription
init(alloc: ref HeapAllocator, program: static_ptr<u8> | ref String)The program is argv[0]
arg(value: static_ptr<u8>) void / (value: ref String) voidAppend one argument
stdin / stdout / stderr(mode: Stdio) voidInherit, Piped or Null
start() SystemResult<Child>Run it, return immediately
status() SystemResult<i32>Run to completion, inheriting the streams
output() SystemResult<Output>Run to completion, capturing both streams

Arguments are passed to the program as they are given — a space inside one argument stays inside it, with no shell reparsing.

The child starts with SIGPIPE at its default disposition even when the parent ignores it. An ignored signal would otherwise survive exec, and a child whose reader goes away would then see EPIPE and complain instead of exiting quietly.

Child

MethodSignatureDescription
id() i32Process id
wait() SystemResult<i32>Exit code; a killed child reports 128 + signal
try_wait() SystemResult<Option<i32>>None while it is still running
kill(sig: i32) SystemResult<void>
write_stdin / close_stdin(data: ref String) i64 / () voidNeeds stdin(Stdio.Piped)
take_stdout / take_stderr() i32Hand the pipe over
collect(alloc: ref HeapAllocator) SystemResult<Output>Drain both pipes and wait

collect polls the two pipes rather than draining one and then the other: a child that fills the stderr pipe while the parent is stuck reading stdout would deadlock. Output gives status() i32 and borrowed stdout() / stderr().

A Child releases any pipes it still holds when it goes out of scope. It never waits and never signals there — a moved-from Child is all zeroes, and reaping or killing on that would touch a process the object no longer stands for. Call wait to reap.

Identity, signals and raw control

FunctionSignatureDescription
pid / parent_pid() i32
uid / euid() u32
kill(target: i32, sig: i32) SystemResult<void>
set_pgid(target: i32, pgid: i32) SystemResult<void>0 means "this one"
create_new_session() SystemResult<i32>setsid
exit / exit_now(code: i32) voidWith / without atexit handlers
fork() SystemResult<i32>0 in the child, the child's pid in the parent
exec(cmd: ref Command) SystemResult<void>Only returns on failure
wait_pid(target: i32, options: i32) SystemResult<i32>Raw wait status; -1 = any child
exited / exit_status / signaled / get_term_signal(status: i32)Read a wait status
open_pipe(alloc: ref HeapAllocator) SystemResult<Pipe>read_fd() / write_fd()
dup_fd(old_fd: i32, new_fd: i32) SystemResult<void>dup2

Signal numbers are SIGHUP, SIGINT, SIGQUIT, SIGKILL, SIGSEGV, SIGPIPE, SIGALRM, SIGTERM, SIGCHLD, SIGCONT, SIGSTOP; wait_pid takes WNOHANG.

Between fork and exec a child may only call things that are safe inside a signal handler — no allocation, no method that might allocate. Command is written to that rule; hand-rolled forks have to be too.

Time

stdlib/time.sun (module std.time).

using std.time;
 
var start = now();
sleep(create_duration_millis(50));
println(start.elapsed().as_millis());
 
println(format(alloc, convert_to_utc(read_unix_time()), "%Y-%m-%d %H:%M:%S"));
ItemSignatureDescription
nanos / micros / millis / seconds(n: i64) DurationBuild a Duration
Duration.as_secs / as_millis / as_micros / as_nanos() i64
Duration.as_secs_f64() f64
Duration.subsec_nanos() i64Remainder on top of as_secs
now() InstantMonotonic clock
Instant.elapsed() DurationTime since it was taken
Instant.since(earlier: const ref Instant) DurationZero if this is the earlier
read_unix_time / read_unix_time_millis() i64Wall clock, since the epoch
sleep(d: const ref Duration) voidA signal can cut it short
utc / local(unix_secs: i64) DateTimeBreak into calendar fields
to_unix_utc / to_unix_local(dt: const ref DateTime) i64And back
format(alloc, dt: const ref DateTime, fmt: raw_ptr<u8>) Stringstrftime

Only differences between Instants mean anything: the zero point is arbitrary, and the clock never jumps backwards when the system time is adjusted.

DateTime uses human units — year is the full year and month is 1-12, not C's tm_year/tm_mon offsets. It also carries day, hour, minute, second, weekday (Sunday = 0), yearday and utc_offset.

Networking

TCP and UDP sockets and hostname resolution, from networking.sun. For TLS and HTTPS see TLS & HTTPS. Fallible networking and HTTP operations return SystemResult<T>. These examples use try inside a function returning a compatible result enum.

using std;
 
// Client
var stream = TcpStream();
var host = String(alloc, "example.com");
try stream.connect_host(host, 80);       // resolve a name, then connect
try stream.connect_ip(ip, 80);           // or an address already in hand
try stream.connect_local(8080);          // or 127.0.0.1
try stream.set_nonblocking(true);         // configure the chosen connection
 
try stream.send_str("ping");
try stream.send_string(message);
var buf = ContiguousBuffer<u8>(alloc, 4096);
var n = try stream.recv(buf);            // bytes read; 0 at end of stream
stream.close();
 
// Server
var listener = TcpListener();
try listener.bind_port(8080);            // 0.0.0.0; bind_loopback for 127.0.0.1
try listener.set_nonblocking(true);       // configure after binding
try listener.listen(128);
var client = try listener.accept();
try client.set_nonblocking(true);         // accepted streams configure themselves

resolve_host_ipv4 does name resolution on its own, returning the first A record in network byte order — the form connect_ip takes:

var ip: i32 = try resolve_host_ipv4(host);

Resolution returns SystemResult.Error when the name does not resolve or has no IPv4 address. Socket failures carry the positive libc errno value in Error.code(); they do not use -1 as an error code.

For a single-threaded event loop, connect a stream or bind a listener first, then call set_nonblocking(true) and register get_fd() with a Poller. Wait with a Duration, handle each returned Event, and keep reading, writing, or accepting until an operation returns SystemResult.Error(e) for which is_would_block(e.code()) is true. An accepted TcpStream has its own descriptor, so configure and register it separately. Calling set_nonblocking(false) restores blocking behavior without changing unrelated descriptor flags.

IPv4 addresses

Ipv4Addr names an address by its dotted-quad octets, so nobody has to remember that 127.0.0.1 is the integer 16777343. It holds the address in network byte order — to_network_i32() returns the form connect_ip and the intrinsics take, and ipv4_from_network wraps what resolve_host_ipv4 returns.

var lan = Ipv4Addr(192, 168, 1, 10);     // from octets
var addr = try parse_ipv4(text);             // propagate an error if malformed
var any = ipv4_any();                    // 0.0.0.0
var home = ipv4_loopback();              // 127.0.0.1
println(lan.to_string(alloc));           // "192.168.1.10"
try stream.connect_addr(lan, 80);            // TCP takes it too
try listener.bind_addr(any, 8080);

UDP

UdpSocket sends and receives datagrams — no connection, one message per call. bind creates the socket, so it always comes first; binding port 0 lets the OS pick a free port — right for a send-only socket — and local_port() reads the pick back. Binds are exclusive by default: an occupied address returns an Error with the native EADDRINUSE code. A failed bind leaves the socket closed, so callers can retry another port. Pass true as the third argument, socket.bind(any, port, true), to enable SO_REUSEADDR for shared listeners. Each socket sharing the address must opt in; ordinary unicast listeners should keep the exclusive default.

using std;
 
var socket = UdpSocket();
var any = ipv4_any();
try socket.bind(any, 0);
var port: i32 = try socket.local_port();
 
var dest = Ipv4Addr(192, 168, 1, 20);
try socket.send_str_to("ping", dest, 9000);      // a literal
try socket.send_to(data, len, dest, 9000);       // raw bytes
 
var buf = ContiguousBuffer<u8>(alloc, 2048);
var datagram = try socket.recv_from(buf);        // one datagram, cut to fit buf
datagram.length();                           // bytes received
datagram.addr();                             // sender's Ipv4Addr
datagram.port();                             // sender's port
socket.close();

A blocking recv_from can be given a deadline with set_recv_timeout(duration); when it expires the call returns an error for which is_would_block(e.code()) is true. set_nonblocking(true) and a Poller work here the same way they do for TCP.

Multicast is part of UdpSocket. Join a group on an interface (use ipv4_any() to let the OS choose one), and the socket receives datagrams sent to the group on its bound port:

var socket = UdpSocket();
try socket.bind(ipv4_any(), 9000, true);
var group = Ipv4Addr(239, 255, 0, 1);
var iface = ipv4_any();
try socket.join_multicast(group, iface);
try socket.set_multicast_ttl(1);        // how many router hops sends may cross
try socket.set_multicast_loop(true);    // hear this host's own sends
try socket.leave_multicast(group, iface);

IPv4 is the only supported address family for now. TCP sockets have no socket-level receive timeout; use Poller for bounded waits there.

HTTP server

http.sun builds an HTTP/1.1 server on TcpListener. The server owns the accept loop and all framing; the handler only reads the request and fills in the response.

var server = HttpServer(alloc);
try server.bind(8080);
server.serve((req: ref HttpRequest, resp: ref HttpResponse) => void {
    if (req.path.equals_literal("/")) {
        resp.set_body("<h1>Hello</h1>");
    } else {
        resp.set_status(404);
        resp.set_body("not found");
    }
});

It handles one connection at a time, parses request line and headers (not bodies), and always answers Connection: close. For outgoing requests, use HttpsClient from the tls bundle.

Optional values and results

Option<T>, defined in option.sun, represents a value that may be absent:

enum Option<T> { Some(T), None }

For fallible operations, prefer the result enums defined in result.sun:

  • AccessResult<T> contains Ok(T) or OutOfBounds(IndexOutOfBoundsError). Checked vector, buffer, and string access operations use this narrow result.
  • LookupResult<T> contains Ok(T) or NotFound(NotFoundError) for map and linked-list lookups, updates, and removal.
  • PopResult<T> contains Ok(T) or Empty(EmptyError) for linked-list and string pops.
  • JsonResult<T> contains Ok(T) or Error(JsonError) for JSON parsing, conversions, and container operations.
  • ConversionResult<T> reports ConversionError through its Error variant.
  • ArithmeticResult<T> contains Ok(T), Overflow(OverflowError), and DivisionByZero(DivisionByZeroError). Use _ => ... to handle all remaining variants in a match.
  • ProtoResult<T> contains Ok(T) or Error(ProtoDecodeError) for wire reads and generated protobuf decoders.
  • SystemResult<T> contains Ok(T) or Error(Error) for environment, process, file, polling, terminal, networking, and HTTP operations.
  • Result<T> contains Ok(T) and named variants for standard error categories, including bounds, lookup, I/O, allocation, conversion, arithmetic, JSON, and protocol decoding errors. Use it when combining several APIs.

Use match to handle results or prefix try to propagate failures. try treats the first declared variant as success; it does not depend on stdlib names. The builtin _Result<T, E> remains available without importing the stdlib.

Iteration: IIterator<T, Container> and IIterable<T, Self>

The iteration protocol lives in iterator.sun and is what for-in drives:

interface IIterator<T, Container> {
    method next(container: ref Container) Option<T>;
}
interface IIterable<T, Self> {
    method iter() IIterator<T, Self>;
}

next() yields the next element or Option.None once the sequence is exhausted (and keeps returning None afterwards). for (var x: T in c) calls iter() when c has one, then loops on next() until None; the loop variable's type must match T. Iterators are separate objects that receive the container by ref on every call, so a container can be iterated more than once and iterators stay cheap:

class Countdown implements IIterator<i32, Countdown> {
    var n: i32;
    init(n: i32) { this.n = n; }
 
    method next(self: ref Countdown) Option<i32> {
        if (this.n == 0) { return Option.None; }
        this.n = this.n - 1;
        return Option.Some(this.n + 1);
    }
}
 
function main() i32 {
    var sum: i32 = 0;
    for (var x: i32 in Countdown(5)) {
        sum = sum + x;
    }
    return sum;  // 5 + 4 + 3 + 2 + 1 = 15
}

Iterators can also be driven by hand:

var it = v.iter();
var going = true;
while (going) {
    match it.next(v) {
        Option.Some(x) => { println(x); },
        Option.None => { going = false; }
    };
}

A container implements IIterable and returns its concrete iterator from iter(); the loop then passes the container to every next() call, so next must take that container type by ref (the compiler checks this — it is what keeps the container access inside next memory-safe):

class Range implements IIterable<i32, Range> {
    var start: i32;
    var end: i32;
    init(s: i32, e: i32) { this.start = s; this.end = e; }
    method iter() RangeIterator { return RangeIterator(this.start); }
}
 
class RangeIterator implements IIterator<i32, Range> {
    var cur: i32;
    init(s: i32) { this.cur = s; }
    method next(r: ref Range) Option<i32> {
        if (this.cur >= r.end) { return Option.None; }
        this.cur = this.cur + 1;
        return Option.Some(this.cur - 1);
    }
}

Because iter() returns a class rather than the IIterator<T, Self> fat pointer the interface names, an IIterable implemented this way is static-only: it drives for-in, but a Range cannot be converted to an IIterable<i32, Range> value (there is no way to dispatch iter() through it soundly).

Vec<T>, LinkedList<T>, Map<K, V> (values) and ContiguousBuffer<T> implement IIterable.

BigUint

Arbitrary-precision unsigned integer from bigint.sun: little-endian u64 limbs in a Vec, schoolbook arithmetic built on _mul_hi_u64. Values are owned (clone() to copy); the by-ref argument of a binary operation is never modified.

var n = BigUint(alloc, 1);
for (var i: u64 = 2; i <= 30; i = i + 1) { n.mul_small(i); }   // 30!
var text = String(alloc, "");
n.append_decimal(text);   // 265252859812191058636308480000000
MethodSignatureDescription
BigUint(alloc) / BigUint(alloc, v)(const ref HeapAllocator[, u64])Zero, or a machine word
clone()() BigUintIndependent copy
is_zero(), bit_length(), limb_count(), limb(i), low_u64()Inspection; limb(i) is 0 beyond the top
to_u64()() Option<u64>The value if it fits in 64 bits
set_u64(v), assign(other), clear()Replace the value
compare(other), compare_u64(v), equals(other)(const ref BigUint) i64, (u64) i64, (const ref BigUint) boolOrdering (-1/0/1)
add(other), add_small(v)(const ref BigUint) void, (u64) voidIn-place addition
sub(other)(const ref BigUint) ArithmeticResult<void>In-place subtraction; returns Overflow if the result would be negative
mul(other), mul_small(v), mul_pow10(n)(const ref BigUint) void, (u64) void, (i64) voidIn-place multiplication
shl(bits), shr(bits)(i64) voidIn-place shifts
divmod_small(d)(u32) ArithmeticResult<u32>Divide in place, returning the remainder or DivisionByZero for a zero divisor
append_decimal(out)(ref String) voidAppend the decimal digits
set_decimal(text)(const ref String) boolParse decimal digits; false (value left at zero) if malformed

Numeric conversion

A value never narrows on its own: assigning an i64 to an i32 is a compile error. math.sun provides safe_convert<T>(x), which converts between any two numeric types and returns ConversionResult<T>. Its Error variant owns a ConversionError when the value does not fit:

/** Narrows a value and lets the caller handle conversion failure. */
function narrow(value: i64) ConversionResult<u8> {
  return safe_convert<u8>(value);
}
 
/** Prints the converted number or the error message. */
function show_conversion(value: i64) void {
  match narrow(value) {
    ConversionResult.Ok(number) => println(number),
    ConversionResult.Error(error) => println(error.message())
  };
}

Inside a function returning a compatible result enum, use try safe_convert<T>(x) to extract the number and propagate conversion failures. For example, converting 255.0 to u8 succeeds; 256.0, negative integers, and NaN return Error.

Checked: integer values outside T's range (including a sign change that flips the value), floats that are NaN or outside T's range, and an f64 that overflows f32. A fraction is truncated toward zero, as with _convert. Widening and integer-to-float conversions always succeed.

safe_convert takes two type parameters, <T, U>; the source type U is inferred from the argument, so only the target is written. When the value is known to fit, the _convert<T>(x) intrinsic does the same conversion with no check (see Intrinsics).

Numeric limits

math.sun also defines I64_MAX, I64_MIN, F64_MAX, F64_MIN_POSITIVE (smallest normal) and F64_EPSILON.

Byte order

A binary wire format fixes the order the bytes of a number appear in, and that order is often not the one the machine uses. stdlib/byte_order.sun (module std.byte_order) converts between the two, for 16-, 32- and 64-bit unsigned integers.

swap_bytes_u16, swap_bytes_u32 and swap_bytes_u64 reverse the bytes unconditionally:

using std.byte_order;
 
var swapped: u32 = swap_bytes_u32(305419896);   // 0x12345678 -> 0x78563412

The to_/from_ pairs name the order the wire uses, and do nothing when it already matches the machine's. Big-endian is what network protocols call network byte order, so to_big_endian_u16 is the htons of C, and from_big_endian_u32 is its ntohl:

var wire: u16 = to_big_endian_u16(port);      // host -> network order
var port: u16 = from_big_endian_u16(wire);    // network -> host order
 
var little: u32 = to_little_endian_u32(n);    // for a little-endian format
var n: u32 = from_little_endian_u32(little);

There are twelve in all: to_big_endian_u16/u32/u64, from_big_endian_u16/u32/u64, and the same four for little-endian. Unlike C, the 64-bit conversions are there too.

Every processor Sun targets is little-endian, so the big-endian conversions reverse the bytes and the little-endian ones compile to nothing. Each reversal is the _bswap_u16 / _bswap_u32 / _bswap_u64 intrinsic (see Intrinsics), a single instruction.

Json

json.sun parses JSON text into an owned tree and writes it back, compact or pretty. Objects keep their members in insertion order (lookups are a linear scan), numbers are Int(i64) when the text is an integer that fits and Float(f64) otherwise, strings are UTF-8 Strings.

/** Parses and builds JSON while propagating concrete JSON errors. */
function json_example() JsonResult<void> {
  var alloc = make_heap_allocator();
  var doc = try parse_json(alloc, "{\"name\": \"sun\", \"tags\": [1, 2.5]}");
  var tags = try doc.get("tags");
  var second: f64 = try (try tags.at(1)).as_f64();
 
  var obj = create_json_object(alloc);
  try obj.set(String(alloc, "ok"), Json(true));
  try obj.set(String(alloc, "tags"), create_json_array(alloc));
  try (try obj.get("tags")).push(Json(42));
  println(obj.to_string(alloc));            // {"ok":true,"tags":[42]}
  println(obj.to_pretty_string(alloc, 2));
  return JsonResult.Ok;
}

The tree node is class Json wrapping a public var value: JsonValue:

enum JsonValue { Null, Bool(bool), Int(i64), Float(f64), String(String), Array(Vec<Json>), Object(Vec<JsonMember>) }
class JsonMember { var key: String; var value: Json; }

Match on j.value directly when the accessors below are not enough.

The tree owns everything in it. Json(s), push(item) and set(key, value) take their String and Json arguments by value, which in Sun means they are moved in: the caller cannot use them afterwards, and the borrow checker rejects a later use. When a value is still needed after being placed in a document, store a copy instead. create_json_string(alloc, ref s) does that for a String:

// cfg.model is still needed later, so the document gets a copy
try req.set(String(alloc, "model"), create_json_string(alloc, cfg.model));
// Equivalent to an explicit clone:
try req.set(String(alloc, "model"), Json(cfg.model.clone(alloc)));
// Moves cfg.model into the document; using it again is a compile error
try req.set(String(alloc, "model"), Json(cfg.model));
Function / MethodSignatureDescription
parse_json(alloc, text)(ref HeapAllocator, ref String) JsonResult<Json>Parse a complete document (also accepts a static_ptr<u8> literal). Rejects trailing text, leading zeros, control characters in strings, and nesting deeper than 256
Json(), Json(b), Json(n), Json(x), Json(s)(), (bool), (i64), (f64), (String)Build a scalar node (Json(s) moves the String into the node; see above)
create_json_array(alloc) / create_json_object(alloc)(ref HeapAllocator) JsonEmpty array / object
create_json_string(alloc, text)(ref HeapAllocator, static_ptr<u8>) Json, (ref HeapAllocator, ref String) JsonString node from a literal, or a copy of a borrowed String (the caller keeps its own)
is_null(), is_bool(), is_int(), is_float(), is_number(), is_string(), is_array(), is_object()() boolKind checks
as_bool(), as_i64(), as_f64()() JsonResult<T>Scalar readers; as_i64 accepts an integral Float, as_f64 accepts an Int
as_string()() JsonResult<ref String>Borrow the string
len()() i64Array items or object members (0 for scalars)
at(i)(i64) JsonResult<ref Json>Array item
get(key)(static_ptr<u8>) JsonResult<ref Json>, (ref String) JsonResult<ref Json>Object member value
has(key)(static_ptr<u8>) boolWhether an object has the key
key_at(i) / value_at(i)(i64) JsonResult<ref String> / (i64) JsonResult<ref Json>Object members by position, in insertion order
push(item)(Json) JsonResult<void>Append to an array
set(key, value)(String, Json) JsonResult<void>Set an object member (replaces an existing key's value in place, otherwise appends)
write(out) / write_pretty(out, indent)(ref String) void / (ref String, i64) voidAppend JSON text to a String
to_string(alloc) / to_pretty_string(alloc, indent)(ref HeapAllocator) String / (ref HeapAllocator, i64) StringJSON text as a new String

Fallible JSON operations return JsonResult<T>. Its Error variant owns a JsonError (code 60) on a kind mismatch or missing key; parse errors carry the byte offset of the problem in offset(). When writing, Float values keep a .0 or exponent so they read back as Float, and non-finite floats become null.

Testing: std.test

The assertions and runner support behind test_function — see Testing for the full feature. What user code calls directly:

FunctionSignatureDescription
assert(cond) / assert(cond, msg)(bool[, static_ptr<u8>]) std.test.AssertionResult<void>Return Error(AssertionError) when false
assert_eq(expected, actual)overloaded per primitive, plus (const ref String, const ref String)Return an assertion error when the values differ, showing both
assert_ne(a, b)overloaded per primitiveReturn an assertion error when the values are equal
fail(msg)(static_ptr<u8>) std.test.AssertionResult<void>Fail unconditionally

AssertionError (code 10) implements IError. The remaining items — TestResult, run_one, report, finish, wants_sequential — exist for the compiler's synthesized test runner; --debug writes that runner's source to <input>_debug/test_runner.sun if you want to see how they fit together.

Error Types

All error types implement IError with code() i32 and message() String methods. message() returns an owned clone of the description, so the text stays usable for as long as the caller keeps it, independent of the error object itself.

Error TypeCodeDescription
ErrorcustomGeneric error with custom code and message
EmptyError1Operations on empty containers
NotFoundError2Failed lookups
IndexOutOfBoundsError3Invalid array/vector indices
DivisionByZeroError4Division by zero
OverflowError5Numeric overflow
InvalidArgumentError6Invalid function arguments
IOError7I/O operations
AllocationError8Memory allocation failures
ConversionError9A value does not fit the type safe_convert targets
JsonError60JSON parse errors (with offset()) and kind mismatches
/** Returns the first value or an owned empty-container error. */
function first(v: ref Vec<i32>) PopResult<i32> {
    return match v.get(0) {
        AccessResult.Ok(value) => PopResult.Ok(value),
        AccessResult.OutOfBounds(_) => PopResult.Empty(EmptyError())
    };
}

Error accepts either a literal or a String composed at runtime. It owns a copy, so its message remains readable after the source string has been dropped. Returning and propagating a result transfers that ownership to the caller.

/** Creates an owned error message from a runtime path. */
function failure(path: const ref String) SystemResult<void> {
    return SystemResult.Error(Error(errno(), path));
}
 
// At the caller:
match failure(path) {
    SystemResult.Ok => {},
    (e: const ref IError) => {
        var msg: String = e.message();
        eprintln(msg);
    }
};

Print Functions

Overloaded print (no newline) and println (with newline) from print.sun. Both accept any of the following argument types:

OverloadDescription
i32, i64, u32, u64Print integer value
f64Print f64
charPrint a character as UTF-8
boolPrint true or false
static_ptr<u8>Print string literal
const ref StringPrint a borrowed String
(no argument, println only)Print a bare newline

Interpolated strings can be passed directly:

var x: i32 = 42;
println(`x = ${x}`);   // prints "x = 42\n"
print("no newline");
println();

eprint and eprintln write to stderr instead, so a caller can tell diagnostics apart from the program's real output. They accept the same argument types as print and println, including numbers, characters, booleans and a const ref String. Numeric stderr output uses stack buffers without constructing a temporary String; floating-point values use the decimal formatting from String.append_f64. Calling eprintln() writes a bare newline.

eprintln("could not open the file");
eprint("error code: ");
eprintln(42);
eprintln(`giving up after ${attempts} tries`);