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();| Method | Signature | Description |
|---|---|---|
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) void | Free allocated memory |
create<T>(args...) | (...) raw_ptr<T> | Allocate and construct a single T |
copy() | () HeapAllocator | Create 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 cleanupUnique<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| Method | Signature | Description |
|---|---|---|
get() | () ref T | Safe dereference - returns a reference to the value |
get_raw() | () raw_ptr<T> | Get the underlying raw pointer |
is_null() | () bool | Check if pointer is null |
deinit() | () void | Free 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| Method | Signature | Description |
|---|---|---|
clone() | () Shared<T> | Another owner of the same value |
lock() | () SharedGuard<T> | Wait for the lock, return a guard that holds it |
count() | () i32 | How many owners there are right now |
is_null() | () bool | Whether the handle has been moved out of |
deinit() | () void | Drop one owner (called automatically) |
SharedGuard<T> method | Signature | Description |
|---|---|---|
get() | () ref T | The shared value, as a reference |
deinit() | () void | Release 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| Method | Signature | Description |
|---|---|---|
push(value) | (T) void | Append element (grows if needed) |
pop() | () Option<T> | Remove and return last element; None when empty (ownership moves to the caller) |
get(index) | (i64) T | Get element at index |
set(index, value) | (i64, T) void | Set element at index |
size() | () i64 | Current number of elements |
capacity() | () i64 | Current allocated capacity |
is_empty() | () bool | Check if empty |
reserve(n) | (i64) void | Ensure capacity for at least n elements |
clear() | () void | Remove 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));| Method | Signature | Description |
|---|---|---|
insert(key, value) | (K, V) void | Insert 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 V | Get value or return default |
remove(key) | (K) LookupResult<V> | Remove the value or return NotFound |
contains(key) | (K) bool | Check if key exists |
size() | () i64 | Number of entries |
capacity() | () i64 | Number of buckets |
is_empty() | () bool | Check if empty |
clear() | () void | Remove 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 encodedA 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.
| Method | Signature | Description |
|---|---|---|
length() | () i64 | Current string length |
capacity() | () i64 | Allocated capacity |
is_empty() | () bool | True 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) u8 | Unchecked read; requires an unsafe block |
unsafe_set_at(i, val) | (i64, u8) void | Unchecked write; requires an unsafe block |
append_char(ch) | (u8) void | Append a single byte |
append(other) | (const ref String) void | Append another String |
append_literal(s) | (static_ptr<u8>) void | Append a string literal |
equals_literal(s) | (static_ptr<u8>) bool | Compare 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) bool | Check if the byte occurs |
push(c) | (char) void | Append the UTF-8 encoding of a char (1 to 4 bytes) |
iterate_chars() | () Chars | Iterate the string as Unicode scalar values |
count_chars() | () i64 | Number 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) bool | Check 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) bool | Check if the substring occurs |
starts_with(s) / ends_with(s) | (static_ptr<u8>) bool, (const ref String) bool | Prefix / suffix check |
append_i64(n) / append_u64(n) | (i64) void | Append an integer in decimal |
append_f64(x) | (f64) void | Append 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"| Method | Signature | Description |
|---|---|---|
to_lower() / to_upper() | () void | Convert ASCII letters in place |
trim() | () void | Drop leading and trailing ASCII whitespace in place |
trim_start() / trim_end() | () void | Drop whitespace from one end in place |
reverse() | () void | Reverse the bytes in place |
replace(from, to) | (static_ptr<u8>, static_ptr<u8>) void, (const ref String, const ref String) void | Replace every occurrence in place; an empty from does nothing |
clone(alloc) | (const ref HeapAllocator) String | Copy 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>) String | Free 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()); // 5Character helpers
Free functions in std for moving between char and code points.
| Function | Signature | Description |
|---|---|---|
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) i64 | The code point of a char |
compute_utf8_len(code) | (i64) i64 | Bytes the UTF-8 encoding of a code point occupies |
encode_utf8_byte(code, j) | (i64, i64) u8 | Byte 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);
}| Method | Signature | Description |
|---|---|---|
next(self) | (ref Chars) Option<char> | Next scalar value; None at the end |
offset() | () i64 | Byte 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.
| Method | Signature | Description |
|---|---|---|
length() | () i64 | View length |
at(i) | (i64) AccessResult<u8> | Read a byte; reject indices outside the view |
unsafe_at(i) | (i64) u8 | Unchecked read relative to the view; requires an unsafe block |
equals_literal(s) | (static_ptr<u8>) bool | Compare 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) bool | Check if the byte occurs |
append_i64(n) / append_u64(n) | (i64) void | Append an integer in decimal |
append_f64(x) | (f64) void | Append 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| Method | Signature | Description |
|---|---|---|
get(indices) | (ref array<i64>) T | Get element at indices |
set(indices, value) | (ref array<i64>, T) void | Set element at indices |
dim(i) | (i64) i64 | Size of dimension i |
size() | () i64 | Total number of elements |
ndims() | () i64 | Number 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| Function | Signature | Description |
|---|---|---|
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> method | Signature | Description |
|---|---|---|
join() | () T | Block 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();| Method | Signature | Description |
|---|---|---|
lock() | () void | Acquire the lock (blocks if contended) |
unlock() | () void | Release 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.
| Method | Signature | Description |
|---|---|---|
open | (path: Path, mode: FileMode) SystemResult<void> | Open a file |
adopt | (fd: i32) void | Take ownership of an open descriptor |
write | (data: static_ptr<u8>) i64 / (data: ref String) i64 | Bytes written |
write_bytes | (buf: raw_ptr<u8>, len: i64) i64 | Write raw bytes |
read_all | (alloc: ref HeapAllocator) SystemResult<String> | Read to end of file |
read_into | (buf: raw_ptr<u8>, len: i64) i64 | Read into a buffer |
seek | (offset: i64, whence: Whence) i64 | New offset from the start |
tell / size | () i64 | Current position / total length |
sync / truncate | () SystemResult<void> / (length: i64) SystemResult<void> | Flush / resize |
close / is_open / get_fd | Close (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:
| Function | Signature |
|---|---|
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());
}| Item | Signature | Description |
|---|---|---|
read_dir | (alloc: ref HeapAllocator, path: Path) SystemResult<Vec<DirEntry>> | . and .. excluded, filesystem order |
DirEntry.name | () ref String | Borrowed — the entry keeps it |
DirEntry.kind | () FileKind | File, 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| Method | Signature | Description |
|---|---|---|
add_read / add_write / add_read_write | (fd: i32) void | Watch a descriptor |
add | (fd: i32, events: i16) void | Watch with an explicit POLL* mask |
modify | (fd: i32, events: i16) bool | Replace interest and clear readiness for every matching registration |
remove | (fd: i32) bool | Remove 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 / count | Reset, 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 restoredTerminalMode 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.
Mode | Clears | Keeps |
|---|---|---|
Raw | echo, line editing, CR/NL translation, Ctrl-S/Ctrl-Q flow control, output processing | |
Cbreak | echo, line editing | output 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:
SignalPolicy | Meaning |
|---|---|
Passthrough | ISIG stays on. Ctrl-C, Ctrl-\ and Ctrl-Z raise SIGINT, SIGQUIT and SIGTSTP for the foreground process group and never arrive as bytes. |
AsInput | ISIG 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.
| Function | Signature | Description |
|---|---|---|
is_terminal | (fd: i32) bool | isatty; 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) bool | Read the ECHO / ICANON / ISIG bits of a snapshot |
TerminalMode | Signature | Description |
|---|---|---|
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 | () bool | Whether 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();| Method | Signature | Description |
|---|---|---|
get | (name: Name) Option<ref String> | First matching entry, or None |
has | (name: Name) bool | Whether 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 | () void | Observe changes made outside this Env |
size / is_empty | () i64 / () bool | Snapshot size |
iter | () EnvIterator | Borrow 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()); // helloCommand
| Method | Signature | Description |
|---|---|---|
init | (alloc: ref HeapAllocator, program: static_ptr<u8> | ref String) | The program is argv[0] |
arg | (value: static_ptr<u8>) void / (value: ref String) void | Append one argument |
stdin / stdout / stderr | (mode: Stdio) void | Inherit, 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
| Method | Signature | Description |
|---|---|---|
id | () i32 | Process 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 / () void | Needs stdin(Stdio.Piped) |
take_stdout / take_stderr | () i32 | Hand 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
| Function | Signature | Description |
|---|---|---|
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) void | With / 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"));| Item | Signature | Description |
|---|---|---|
nanos / micros / millis / seconds | (n: i64) Duration | Build a Duration |
Duration.as_secs / as_millis / as_micros / as_nanos | () i64 | |
Duration.as_secs_f64 | () f64 | |
Duration.subsec_nanos | () i64 | Remainder on top of as_secs |
now | () Instant | Monotonic clock |
Instant.elapsed | () Duration | Time since it was taken |
Instant.since | (earlier: const ref Instant) Duration | Zero if this is the earlier |
read_unix_time / read_unix_time_millis | () i64 | Wall clock, since the epoch |
sleep | (d: const ref Duration) void | A signal can cut it short |
utc / local | (unix_secs: i64) DateTime | Break into calendar fields |
to_unix_utc / to_unix_local | (dt: const ref DateTime) i64 | And back |
format | (alloc, dt: const ref DateTime, fmt: raw_ptr<u8>) String | strftime |
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 themselvesresolve_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>containsOk(T)orOutOfBounds(IndexOutOfBoundsError). Checked vector, buffer, and string access operations use this narrow result.LookupResult<T>containsOk(T)orNotFound(NotFoundError)for map and linked-list lookups, updates, and removal.PopResult<T>containsOk(T)orEmpty(EmptyError)for linked-list and string pops.JsonResult<T>containsOk(T)orError(JsonError)for JSON parsing, conversions, and container operations.ConversionResult<T>reportsConversionErrorthrough itsErrorvariant.ArithmeticResult<T>containsOk(T),Overflow(OverflowError), andDivisionByZero(DivisionByZeroError). Use_ => ...to handle all remaining variants in a match.ProtoResult<T>containsOk(T)orError(ProtoDecodeError)for wire reads and generated protobuf decoders.SystemResult<T>containsOk(T)orError(Error)for environment, process, file, polling, terminal, networking, and HTTP operations.Result<T>containsOk(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| Method | Signature | Description |
|---|---|---|
BigUint(alloc) / BigUint(alloc, v) | (const ref HeapAllocator[, u64]) | Zero, or a machine word |
clone() | () BigUint | Independent 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) bool | Ordering (-1/0/1) |
add(other), add_small(v) | (const ref BigUint) void, (u64) void | In-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) void | In-place multiplication |
shl(bits), shr(bits) | (i64) void | In-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) void | Append the decimal digits |
set_decimal(text) | (const ref String) bool | Parse 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 -> 0x78563412The 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 / Method | Signature | Description |
|---|---|---|
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) Json | Empty array / object |
create_json_string(alloc, text) | (ref HeapAllocator, static_ptr<u8>) Json, (ref HeapAllocator, ref String) Json | String 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() | () bool | Kind 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() | () i64 | Array 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>) bool | Whether 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) void | Append JSON text to a String |
to_string(alloc) / to_pretty_string(alloc, indent) | (ref HeapAllocator) String / (ref HeapAllocator, i64) String | JSON 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:
| Function | Signature | Description |
|---|---|---|
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 primitive | Return 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 Type | Code | Description |
|---|---|---|
Error | custom | Generic error with custom code and message |
EmptyError | 1 | Operations on empty containers |
NotFoundError | 2 | Failed lookups |
IndexOutOfBoundsError | 3 | Invalid array/vector indices |
DivisionByZeroError | 4 | Division by zero |
OverflowError | 5 | Numeric overflow |
InvalidArgumentError | 6 | Invalid function arguments |
IOError | 7 | I/O operations |
AllocationError | 8 | Memory allocation failures |
ConversionError | 9 | A value does not fit the type safe_convert targets |
JsonError | 60 | JSON 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:
| Overload | Description |
|---|---|
i32, i64, u32, u64 | Print integer value |
f64 | Print f64 |
char | Print a character as UTF-8 |
bool | Print true or false |
static_ptr<u8> | Print string literal |
const ref String | Print 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`);