LEVIATHAN v0.1 · in development

Standard Library

Streams, I/O & Network

Everything that talks to the outside world — the console, a file, a socket, a timer, a child process — flows through one consistent idea: the stream. Learn StreamBuffer<T> once and console, files, sockets, HTTP, and timers all read as the same shape wearing different clothes.

The stream boundary

StreamBuffer<T> is a single-consumer queue — in-language, Array-backed today. Everything else in this page is a typed view over one:

ViewGives you
InStream<T> : IDisposablepull(), hasData(), subscribe((T) => void), close()
OutStream<T>(<<) — insert; returns the stream, so it's chainable
IOStream<T> : InStream<T>, OutStream<T>both ends over one collapsed buffer

StreamBuffer<T> itself exposes push(v), pull(), count(), isEmpty(), close(). A reader's >> extract operator is sugar for pull(). subscribe is a standing pull: it claims the consumer end of the stream, so a later manual pull() throws — any broadcast/fanout behavior you see elsewhere in the standard library is a reshaping built on top, not a property of the buffer itself. Pulling an empty stream throws.

StreamBuffer<int> buf = StreamBuffer<int>();
buf.push(1);
buf.push(2);
int first = buf.pull();   // 1
buf.isEmpty();             // false
buf.count();                // 1

Disposal & unsubscribe

InStream<T> is IDisposable, so a subscription is a resource — using releases it on every scope-exit edge:

using InStream<int> w = signal::on(WINCH);
w.subscribe((int sig) => console.writeln("caught ${sig}"));

close() is idempotent and never throws — the using contract requires that. It runs an optional producer-attached teardown (a signal::on stream's close() calls signal::off under the hood), then closes the backing buffer. On a closed StreamBuffer, push becomes a silent drop — a closed consumer receives zero further deliveries, even mid-broadcast — while pull/setHandler throw the distinct "stream is closed". For an IOStream, close() disposes the read-view subscription and closes the shared buffer; any further << pushes drop silently.

Engine coverage

The stream machinery is full on the oracle, IR, and LLVM engines; emit-C++ compiles the in-memory surface but the signal stream itself stays loop-bound-rejected there.

Console

console is a real prelude object of class Console — resolved, type-checked, and aliasable like any other value, not a compiler special case:

class Console {
    void write<T>(T v);        // native: stringify + write to stdout
    void writeln<T>(T v);      // native: stringify + newline
    void writeln();             // native: bare newline
    Console (<<)(string s);    // transfer operator — chains: console << a << b
}
const Console console = Console();

write/writeln stringify any single value through the generic T; (<<) gives it the same out-stream operator surface as everything else on this page:

console.writeln("booting");
console << "value = " << 42 << "\n";

Console c = console;   // aliasing works — console is an ordinary value
c.writeln("via alias");

console is const: a fact declared once at prelude init, not a variable. console = Console(); is a compile error (cannot assign to const 'console'). Inside comptime code, console output is emitted during compilation.

console << s runs on the oracle, IR, and pure-ELF engines; emit-C++ does not yet cover object transfer operators and reports its coverage error instead, the same as the stream types themselves.

Files

OpenMode is a flag type combined with the (|) operator method — not a union type — so File stays type-safe over an OpenMode instead of a bare int:

using File f = File("log.txt", std::write | std::append);
f.writeln("started");

Flags: std::read, std::write, std::append, std::binary (inert until Block lands). File(path, mode) opens on construction; failure throws FileException : Exception, IFileException.

MemberMeaning
open() / close() / isOpen()lifecycle
write(s) / writeln(s)text output
readln()next line; "" at end
read(max)up to max characters
reader() -> FileInStreampull() / >> extract
writer() -> FileOutStream<< insert
exists() / size() / modified()attributes on an open File

The same attributes exist at path level without opening anything: std::fileExists(path), std::fileSize(path), std::fileModified(path), std::isDir(path).

File is IDisposableusing File f = File(path, mode); closes it deterministically on every scope exit, the same discipline as the stream subscriptions above. planned locking (alongside concurrency) and binary mode + seek (with Block).

Environment, time & the system

A family of floor natives under std, all sys*-prefixed so comptime code is denied automatically by the hermeticity gate — a build never reads the clock, entropy, the filesystem, a tty, or DNS. Optional returns carry the three-state fact: None is distinct from "" / [].

CallMeaning
env::get(key) -> string?None when unset — distinct from set-but-empty
env::args() -> Array<string>process argv
env::name() -> stringprogram name
std::sysMonotonic() -> intCLOCK_MONOTONIC ms; never jumps on a wall-clock adjust engines
std::sysRandom(n) -> stringn cryptographically-random bytes; n <= 0"", n > 1MB throws engines
std::sysIsTty(fd) -> boolis fd a terminal engines
std::sysMkdir(path) -> int0/-1 engines
std::sysRemove(path) -> intunlinks a file, falls back to rmdir on a directory engines
std::sysRename(from, to) -> int0/-1 engines
std::sysListDir(path) -> Array<string>?entry names (no ./..); None if not a directory engines
std::sysResolve(host) -> string?first A record, dotted-quad; None on failure engines
string? home = env::get("HOME");
if (home is string h) { console.writeln("home=${h}"); }

Array<string>? entries = std::sysListDir(".");
string bytes = std::sysRandom(16);
Interpreter-first

This whole family runs on the tree-walk (oracle) and IR interpreters — the design's semantic reference. sysMonotonic additionally runs on LLVM-native binaries. The remaining natives on compiled backends (emit-C++, LLVM) and the frozen pure-ELF backend report a clean coverage error naming the native — never a miscompile.

Timers & the event loop

A timer is a system stream: the runtime loop pushes tick numbers (1, 2, …) into a real StreamBuffer, so subscribe/ pull are the ordinary stream machinery you already know.

std::after(2000).subscribe((int tick) => console.writeln("fired once"));

Timer heartbeat = std::every(1000);
heartbeat.subscribe((int tick) => console.writeln("tick ${tick}"));
// heartbeat.cancel(); // stop it early

std::after(ms) is one-shot; std::every(ms) repeats. Timer exposes ticks() -> InStream<int>, subscribe((int) => void), cancel().

The implicit loop

After top-level code finishes, the program keeps running while live work remains — pending timers, open sockets — and exits once none does. One-shots release themselves after firing; cancel() releases a repeating timer. Dispatch is single-threaded, so callbacks never race. When several timers are due together they fire in (due-time, creation) order. An uncaught exception in a callback stops the loop and reports the usual way.

Sockets

Sockets run on the same event loop, driven by fd read- and write-watches.

std::connectTimeout("example.com", 443, 3000, (int fd) => {
    if (fd == -1) { console.writeln("connect failed"); return; }
    TcpStream conn = TcpStream(fd);
    conn.onData((string chunk) => console.writeln("got: ${chunk}"));
    conn.onClose(() => console.writeln("closed"));
    conn << "hello\n";
});

TcpStream(<<)/send, onData((string) => void), onClose(() => void), close(); reads come from sysRecv -> string?, where None means the peer closed. send never silently short-writes: the fd is non-blocking, so a large payload's unsent tail is buffered and a write-watch drains it as the kernel makes room; a fatal send (peer gone) drops the buffer and the read side delivers the close.

TcpListener is a stream of connections: connections((TcpStream) => void), stop().

TcpListener server = TcpListener(8080);
server.connections((TcpStream conn) => {
    conn << "welcome\n";
    conn.onData((string s) => conn << s);   // echo
});

std::connectTimeout(host, port, ms, (int) => void cb) connects with a deadline; cb receives the connected fd (ready for TcpStream) or -1 on refusal, unreachability, a bad literal, or the deadline. It's built entirely in-language over the non-blocking connect floor — nothing here needs a new native. A bare host literal containing : selects IPv6.

HTTP

A full HTTP stack, built in-language directly over TCP streams — framework-grade, zero new natives.

Requests, responses & chunked bodies

TypeNotes
HeaderMapordered, case-insensitive multimap of Header { name; value }: add/set (replace-all-then-append), first(name) -> string?, all(name) -> Array<string>, has, remove, entries() (order + duplicates kept, so Set-Cookie survives), render()
HttpRequestmethod/path/version/body/headers, header(name) -> string; parse(raw) for a buffered request, or an incremental feed(chunk) -> bool (head then body by Content-Length; true when complete — v1 does not pipeline)
HttpResponse(status, body)headers, withHeader(name, value), reason(), render() (computes Content-Length + Connection), parse(raw)decodes Transfer-Encoding: chunked transparently
ChunkedDecoderfragmentation-proof: feed(chunk), isDone; std::chunkEncode(data) / std::chunkEnd() on the encode side

Server & client

HttpServer server = HttpServer(8080);
server.handle((HttpRequest req) => {
    if (req.path == "/") { return HttpResponse(200, "hello"); }
    return HttpResponse(404, "not found");
});

HttpServer(port) takes one handler, (HttpRequest) => HttpResponse. It re-arms a connection for the next request when neither side sent Connection: close (server-side keep-alive, bounded at 100 requests/connection), and an uncaught throw inside the handler becomes a 500 + Connection: close without taking the loop down.

HttpClient client = HttpClient();
HttpResponse resp = await client.fetch("example.com", 80, "/status");
console.writeln(resp.status);

HttpClient gives you request(method, host, port, path, HeaderMap, body, cb), get/post sugar over it, and the awaitable fetch(host, port, path) -> Promise<HttpResponse> for await-style code.

Still text bodies, still growing

Bodies stay text until Block lands. Deferred (roadmap): client redirects, URL-string parsing, request timeout, HTTP pipelining, client-side chunk-send, client connection pooling.

TLS & HTTPS

TLS arms in place — the fd IS the socket fd, no new descriptor. Once a session is armed over a connected/accepted socket, sends, receives, and closes route through it transparently and the same fd watches keep working, so TcpStream, HttpServer, and HttpClient gain TLS with zero API change. It's backed by system OpenSSL (≥ 1.1.1) behind a narrow provider seam; a build without OpenSSL ships a clean not-built provider and plaintext programs are unaffected.

HttpServer secure = HttpServer(8443, "cert.pem", "key.pem");

HttpClient client = HttpClient();
HttpResponse resp = await client.fetchTls("example.com", 443, "/");

HttpServer(port, cert, key) serves HTTPS; HttpClient adds requestTls/getTls/postTls/fetchTls — always with full verification. Posture is normative: TLS 1.2 floor with 1.3 on, verification ON by default, no renegotiation or compression. SSLKEYLOGFILE (env) enables NSS key-log lines, off by default.

verifyModeMeaning
0full — chain + RFC 6125 hostname (HttpClient's only mode)
1chain-only
2encrypt-only — no verification; never a default

Crypto natives

std::sysRsaEncrypt(pubKeyPem, bytes, padding="oaep") -> string? — RSA public-key encrypt for auth key-transport ("oaep" = OAEP/SHA-1, "pkcs1" = PKCS#1 v1.5; None on parse/capacity/encrypt failure). std::sysRandom(n) is guaranteed crypto-grade — kernel CSPRNG (getrandom / /dev/urandom fallback) — on every covered engine including LLVM.

Coverage & roadmap

TLS and crypto are full on oracle + IR + LLVM engines; emit-C++ and the frozen ELF backend carry the existing clean deferral. Deferred: mTLS, session resumption, cert hot-reload, a cipher-policy surface, OCSP, Windows/macOS native providers, HTTP/2, Block-era AEAD.

Child processes

Process(path, args) spawns a child by explicit path — there is no PATH search; that's the honest v1 boundary (a PATH-walking helper is writable in-language over env::get + std::sysStat). argv is [path] + args, and stdin/stdout/stderr are three pipes wearing the same stream surface as a socket.

Process p = Process("/bin/cat", []);
if (p.ok()) {
    p.onStdout((string s) => console.writeln("out: ${s}"));
    p.write("hello\n");
    p.closeStdin();
    int code = await p.exitCode();
    console.writeln("exit=${code}");
}
MemberMeaning
ok() -> boolspawn succeeded — a bad path is not a spawn failure; the child's exec fails and its exit code arrives as 127
write(string)stdin, queue-and-drain like a socket send
closeStdin()sends EOF — the protocol for /bin/cat-style children
onStdout / onStderrchunk streams riding the ordinary read-watch loop
exitCode() -> Promise<int>non-blocking reap via a pidfd watch; 128+signal if signal-terminated, 127 on spawn failure; delivers buffered output first, then closes every owned fd
kill()SIGTERM; a pending exitCode() then resolves 143
Interpreters only

Process runs on the oracle and IR interpreters engines; compiled backends defer cleanly. A program that never calls exitCode() keeps no reap machinery alive and may exit with live children — standard Unix behavior; they simply go unreaped.

Process exit

Two ways to end the program, one abrupt and one cooperative:

env::setExitCode(2);        // record, keep running, let the loop drain
// ...
env::exit(0);                // stop right now
env::exit(int code)
Terminates immediately and returns code & 0xFF to the host process. It abandons pending event-loop work but still runs the exit epilogue first, including terminal raw-mode restoration. exit does not return.
env::setExitCode(int code)
Records code & 0xFF for normal completion, then lets execution — and the implicit event loop — continue. Multiple calls use the last value.

If an exception reaches top level uncaught, the program reports the usual Uncaught ... message and exits with status 1. A program that falls off the end without setting a code exits 0. Both functions sit under env alongside env::args()/env::name(), floored on std::sysExit/std::sysSetExitCode — denied under comptime by the same hermeticity gate as the rest of this page.