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:
| View | Gives you |
|---|---|
InStream<T> : IDisposable | pull(), 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(); // 1Disposal & 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.
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.
| Member | Meaning |
|---|---|
open() / close() / isOpen() | lifecycle |
write(s) / writeln(s) | text output |
readln() | next line; "" at end |
read(max) | up to max characters |
reader() -> FileInStream | pull() / >> 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 IDisposable — using 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 "" /
[].
| Call | Meaning |
|---|---|
env::get(key) -> string? | None when unset — distinct from set-but-empty |
env::args() -> Array<string> | process argv |
env::name() -> string | program name |
std::sysMonotonic() -> int | CLOCK_MONOTONIC ms; never jumps on a wall-clock adjust engines |
std::sysRandom(n) -> string | n cryptographically-random bytes; n <= 0 → "", n > 1MB throws engines |
std::sysIsTty(fd) -> bool | is fd a terminal engines |
std::sysMkdir(path) -> int | 0/-1 engines |
std::sysRemove(path) -> int | unlinks a file, falls back to rmdir on a directory engines |
std::sysRename(from, to) -> int | 0/-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);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().
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
| Type | Notes |
|---|---|
HeaderMap | ordered, 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() |
HttpRequest | method/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 |
ChunkedDecoder | fragmentation-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.
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.
| verifyMode | Meaning |
|---|---|
0 | full — chain + RFC 6125 hostname (HttpClient's only mode) |
1 | chain-only |
2 | encrypt-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.
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}");
}| Member | Meaning |
|---|---|
ok() -> bool | spawn 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 / onStderr | chunk 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 |
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 nowenv::exit(int code)- Terminates immediately and returns
code & 0xFFto the host process. It abandons pending event-loop work but still runs the exit epilogue first, including terminal raw-mode restoration.exitdoes not return. env::setExitCode(int code)- Records
code & 0xFFfor 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.