Standard Library

Standard Library

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

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

Set SUN_PATH to the directory containing stdlib.moon. When building from source, this is the build/ directory.

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)

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
isEmpty()() 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 val = m.getOrDefault(42, 0);   // 100
m.remove(42);
MethodSignatureDescription
insert(key, value)(K, V) voidInsert or update a key-value pair
get(key)(K) V, IErrorGet value by key (throws if not found)
find(key)(K) Option<V>Get value by key; None if not present
getOrDefault(key, default)(K, V) VGet value or return default
remove(key)(K) V, IErrorRemove and return value (throws if not found)
contains(key)(K) boolCheck if key exists
size()() i64Number of entries
capacity()() i64Number of buckets
isEmpty()() boolCheck if empty
clear()() voidRemove all entries
iter()() MapIterator<K, V>Get iterator over values

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);          // 72 ('H')
s.append_char(33);         // Append '!'
MethodSignatureDescription
length()() i64Current string length
capacity()() i64Allocated capacity
isEmpty()() boolTrue when the string has no bytes
at(i)(i64) u8Get byte at index
set_at(i, val)(i64, u8) voidSet byte at index
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
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
c_str()() raw_ptr<u8>NUL-terminated pointer for C, valid until the next mutation

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 sun.env.get and read_dir hand back text from 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) String, IErrorCopy a byte range; throws if it falls outside the string
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

StringView

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

MethodSignatureDescription
length()() i64View length
at(i)(i64) u8Get byte at index (relative to view)
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
shape()() array<i64>Get dimensions
size()() i64Total number of elements
ndims()() i64Number of dimensions

See Matrices for detailed documentation.

Mutex

Thread synchronization primitive using atomic compare-and-swap with futex-based blocking.

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

See Threads for usage with spawned threads.

File I/O

stdlib/io.sun (module sun.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.

using sun;
using sun.io;
 
var f = File();
try {
    f.open("notes.txt", FileMode.Write);
    f.write("Hello, World!");
    f.close();
} catch (e: IError) {
    eprintln("could not write notes.txt");
}
 
// Or in one step
var text = read_to_string(alloc, "notes.txt");
 
// A runtime path
var path = String(alloc, "/tmp/");
path.append_literal("report.csv");
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) void, IErrorOpen 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) String, IErrorRead 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() void, IError / (length: i64) void, IErrorFlush / 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) String, IError
write_string(path: Path, contents: ref String) void, IError
remove_file / remove_dir(path: Path) void, IError
rename_file(old_path: Path, new_path: Path) void, IError
make_dir(path: Path, mode: i32) void, IError
exists / is_dir / is_file(path: Path) bool
file_size(path: Path) i64, IError
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 = 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) Vec<DirEntry>, IError. 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

Poller wraps poll(2). Descriptors keep the position they were added at, 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 = 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 */ }
MethodSignatureDescription
add_read / add_write / add_read_write(fd: i32) voidWatch a descriptor
add(fd: i32, events: i16) voidWatch with an explicit POLL* mask
wait(timeout_ms: i32) i32, IErrorNumber ready; -1 blocks
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.

Environment

stdlib/env.sun (module sun.env).

using sun.env;
 
var home = match get(alloc, "HOME") {
    Option.Some(p) => p,
    Option.None => String(alloc, "/")
};
 
try { set("LANG", "C"); } catch (e: IError) { }
var here = cwd(alloc);
FunctionSignatureDescription
get(alloc: ref HeapAllocator, name: Name) Option<String>None if unset
has(name: Name) boolSet at all, whatever the value
set(name: Name, value: Name) void, IErrorReplaces any existing value
remove(name: Name) void, IErrorRemoving an unset variable is fine
cwd(alloc: ref HeapAllocator) String, IErrorWorking directory
set_cwd(path: Name) void, IErrorProcess-wide, and it persists
args(alloc: ref HeapAllocator, argc: i32, argv: raw_ptr<raw_ptr<i8>>) Vec<String>argv[0] first

Name stands for the two overloads each of these has: static_ptr<u8> (a literal) and ref String (a name or value built at runtime). set has all four combinations.

args takes argc and argv from main rather than discovering them. Under the JIT the running process is the compiler, so its own argv is not the script's — see Entry point.

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

Listing every environment variable needs the environ global, and Sun has no extern global declaration yet.

Processes

stdlib/process.sun (module sun.process). Command covers running a program; the raw POSIX calls are there for programs that drive the fork themselves.

using sun.process;
 
var cmd = Command(alloc, "/bin/sh");
cmd.arg("-c");
cmd.arg("printf hello");
var out = 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() Child, IErrorRun it, return immediately
status() i32, IErrorRun to completion, inheriting the streams
output() Output, IErrorRun 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.

Child

MethodSignatureDescription
id() i32Process id
wait() i32, IErrorExit code; a killed child reports 128 + signal
try_wait() Option<i32>, IErrorNone while it is still running
kill(sig: i32) void, IError
write_stdin / close_stdin(data: ref String) i64 / () voidNeeds stdin(Stdio.Piped)
take_stdout / take_stderr() i32Hand the pipe over
collect(alloc: ref HeapAllocator) Output, IErrorDrain 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) void, IError
set_pgid(target: i32, pgid: i32) void, IError0 means "this one"
new_session() i32, IErrorsetsid
exit / exit_now(code: i32) voidWith / without atexit handlers
fork() i32, IError0 in the child, the child's pid in the parent
exec(cmd: ref Command) void, IErrorOnly returns on failure
wait_pid(target: i32, options: i32) i32, IErrorRaw wait status; -1 = any child
exited / exit_status / signaled / term_signal(status: i32)Read a wait status
open_pipe(alloc: ref HeapAllocator) Pipe, IErrorread_fd() / write_fd()
dup_fd(old_fd: i32, new_fd: i32) void, IErrordup2

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 sun.time).

using sun.time;
 
var start = now();
sleep(millis(50));
println(start.elapsed().as_millis());
 
println(format(alloc, utc(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
unix_time / 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.

Option<T> and Result<T, E>

Payload enums for absence and fallibility, defined in option.sun:

enum Option<T> { Some(T), None }
enum Result<T, E> { Ok(T), Err(E) }

Inspect them with match; construction infers type arguments from the payload (Option.Some(42)) or from the expected type (Option.None in return position or against an annotation):

var v = match m.find(key) {
    Option.Some(x) => x,
    Option.None => fallback
};

Use Option for "might not be there" (lookups, first/last, pop, iterator next), and IError throwing for actual failures. Result<T, E> suits fallible operations whose error is a value rather than an exception.

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> {
    function next(container: ref Container) Option<T>;
}
interface IIterable<T, Self> {
    function 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;
    function init(n: i32) { this.n = n; }
 
    function 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;
    function init(s: i32, e: i32) { this.start = s; this.end = e; }
    function iter() RangeIterator { return RangeIterator(this.start); }
}
 
class RangeIterator implements IIterator<i32, Range> {
    var cur: i32;
    function init(s: i32) { this.cur = s; }
    function 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) void, IErrorIn-place subtraction; throws OverflowError 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) u32, IErrorDivide in place by a 32-bit divisor, returning the remainder
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 throws ConversionError when the value does not fit:

var ms: i64 = 200;
var t: i32 = safe_convert<i32>(ms);       // 200
var big: i64 = 4294967498;
var u: i32 = safe_convert<i32>(big);      // throws ConversionError
var n: u64 = safe_convert<u64>(-1);       // throws: negative to unsigned
var b: u8 = safe_convert<u8>(255.0);      // 255
var c: u8 = safe_convert<u8>(256.0);      // throws
var h: f32 = safe_convert<f32>(1.0e300);  // throws: infinite in f32

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.

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.

var alloc = make_heap_allocator();
var doc = json_parse(alloc, "{\"name\": \"sun\", \"tags\": [1, 2.5]}");
var name: ref String = doc.get("name").as_string();
var second: f64 = doc.get("tags").at(1).as_f64();
 
var obj = json_object(alloc);
obj.set(String(alloc, "ok"), Json(true));
obj.set(String(alloc, "tags"), json_array(alloc));
obj.get("tags").push(Json(42));
println(obj.to_string(alloc));            // {"ok":true,"tags":[42]}
println(obj.to_pretty_string(alloc, 2));

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. json_string(alloc, ref s) does that for a String:

// cfg.model is still needed later, so the document gets a copy
req.set(String(alloc, "model"), json_string(alloc, cfg.model));
// Equivalent to an explicit clone:
req.set(String(alloc, "model"), Json(cfg.model.clone(alloc)));
// Moves cfg.model into the document; using it again is a compile error
req.set(String(alloc, "model"), Json(cfg.model));
Function / MethodSignatureDescription
json_parse(alloc, text)(ref HeapAllocator, ref String) Json, IErrorParse 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)
json_array(alloc) / json_object(alloc)(ref HeapAllocator) JsonEmpty array / object
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()() T, IErrorScalar readers; as_i64 accepts an integral Float, as_f64 accepts an Int
as_string()() ref String, IErrorBorrow the string
len()() i64Array items or object members (0 for scalars)
at(i)(i64) ref Json, IErrorArray item
get(key)(static_ptr<u8>) ref Json, IError, (ref String) ref Json, IErrorObject member value
has(key)(static_ptr<u8>) boolWhether an object has the key
key_at(i) / value_at(i)(i64) ref String, IError / (i64) ref Json, IErrorObject members by position, in insertion order
push(item)(Json) void, IErrorAppend to an array
set(key, value)(String, Json) void, IErrorSet 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

Readers throw 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.

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
function pop_first(v: ref Vec<i32>) i32, IError {
    if (v.isEmpty()) {
        throw EmptyError();
    }
    return v.get(0);
}

Error takes its message either as a literal or as a String put together at runtime. Given a String it keeps its own copy, so the message stays readable after the String it came from is gone — including in a caller that catches the error several frames up.

var path = String(alloc, "/tmp/");
path.append(name);
throw Error(errno(), path);
 
// ... several frames up:
catch (e: IError) {
    var msg: String = e.message();   // an owned clone
    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
boolPrint true or false
static_ptr<u8>Print string literal
ref StringPrint a 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 take a string literal or a ref String; interpolation covers everything else.

eprintln("could not open the file");
eprintln(`giving up after ${attempts} tries`);