LEVIATHAN v0.1 · in development

Standard Library

JSON, Time & Bytes

Four small, in-language libraries that round out day-to-day work: a total JSON parser and value model, a UTC-only date/time pair, byte-true text codecs and digests, and the transcendental-math and byte-buffer primitives everything else is built on.

JSON

The JSON value model is a plain class, JsonValue — Leviathan has no type aliases, so a recursive named union can't be declared. A kind int tag distinguishes the six JSON shapes (0 null, 1 bool, 2 number, 3 string, 4 array, 5 object).

json::parse is total: malformed input returns None, it never throws. Once you have a value, navigation is deliberately two-speed — at(int)/at(string) throw loudly on a kind mismatch, an out-of-bounds index, or a missing key, while the asXxx() accessors and atOrNone return None instead of raising.

JsonValue? doc = json::parse(text);          // None on malformed — parse never throws
if (doc != None) {
    string name = doc.at("user").at("name").asStr() ?? "?";  // loud navigation + typed access
    string out  = doc.render();                // compact
    string pp   = doc.renderPretty(2);          // indented, 2-space
}

Values are built with labeled "static" constructors rather than a public constructor per kind:

JsonValue v = JsonValue::ofObject(m);
// siblings: ofNull() / ofBool(b) / ofNum(n) / ofStr(s) / ofArray(a) / ofObject(m)
memberbehavior
isNull()isObject()kind test, one per tag
asBool() -> bool?None on kind mismatch
asNum() -> float?None on kind mismatch
asStr() -> string?None on kind mismatch
asArray() -> Array<JsonValue>?None on kind mismatch
asObject() -> Map<string, JsonValue>?None on kind mismatch
at(int) -> JsonValue / at(string) -> JsonValuethrows on kind mismatch, out-of-bounds, or missing key — loud navigation
atOrNone(key) -> JsonValue?the non-throwing sibling of at
size() -> intelement/member count
render() -> stringcompact JSON text
renderPretty(int indent) -> stringindented JSON text (renderPretty(2) is typical)

The parser is recursive-descent with the full JSON escape set, including \uXXXX surrogate-pair combining (a lone/unpaired surrogate decodes to U+FFFD rather than failing); numbers are IEEE doubles; nesting is capped at depth 128; and strict trailing garbage after the top-level value makes the whole parse return None.

Number rendering uses float.toString()'s form (42.000000) for v1 — pinned by the test corpus, and revisited only when float formatting is addressed globally.

engines JSON has full coverage on the oracle, IR, emit-C++, and LLVM engines. It does not run on the frozen ELF backend, which never gained the post-freeze byteAt primitive the value model leans on.

DateTime & Duration

DateTime and Duration are UTC-only value structs built on the public-domain Howard Hinnant civil↔days integer algorithm — no lookup tables, leap seconds ignored, Unix epoch convention throughout.

DateTime t = DateTime::now();                 // labeled ctors: now(), ofEpochMs(int)
DateTime u = DateTime::ofEpochMs(784111777000);
int y = u.year(); u.month(); u.day(); u.hour(); u.minute(); u.second(); u.milli(); u.weekday();

string h = u.httpDate();   // "Sun, 06 Nov 1994 08:49:37 GMT" (RFC 7231)
string i = u.iso8601();    // "1994-11-06T08:49:37Z" (adds ".mmm" if nonzero)

DateTime? p = datetime::parseHttpDate(h);   // None on malformed
DateTime? q = datetime::parseIso8601(i);    // both are datetime:: free functions, no `static`

Duration is built from labeled unit constructors and composes with plus/minus; its toString() renders a compact 1h02m00s form:

Duration d = Duration::ofHours(1).plus(Duration::ofMinutes(2));  // ofMillis/Seconds/Minutes/Hours/Days
string ds  = d.toString();          // "1h02m00s"

Duration span = t.minus(u);         // DateTime.minus -> Duration
DateTime later = u.plus(span);      // DateTime.plus(Duration) -> DateTime

DateTime::now() reads std::sysNow(), the wall-clock epoch-millisecond native every timing facility ultimately depends on. Coverage: all four active engines (oracle, IR, emit-C++, LLVM) — not the frozen ELF backend engines.

Encoding & digests

Byte-true text codecs and cryptographic digests, implemented entirely in-language. Every decoder is total: malformed input returns None rather than throwing, because a data error is a value here, not an exception (Errors & Resources).

namespace encoding {
    string  base64Encode(string bytes);    string? base64Decode(string b64);
    string  base64UrlEncode(string bytes); string? base64UrlDecode(string s);   // JWT: no '=' pad
    string  percentEncode(string s);       string? percentDecode(string s);    // RFC 3986
    string  hexEncode(string bytes);       string? hexDecode(string s);
}

namespace digest returns raw bytes, not hex or base64 text — compose the result with encoding::hexEncode or encoding::base64Encode to get a printable digest:

namespace digest {
    string md5(string);   string sha1(string);   string sha256(string);
    string hmacSha256(string key, string msg);
}

string etag = encoding::hexEncode(digest::sha256(body));
Digests are raw bytes

Forgetting the encoding::hexEncode/base64Encode wrap is the most common mistake here — printing a digest::sha256(...) result directly prints raw byte content, not the familiar hex string.

Digest implementations use the int64 mask idiom (words masked to 32 bits after each operation) and are verified against the RFC 1321/3174 + FIPS 180-4 + RFC 4231 test vectors, including the padding-boundary trap set. They're slow-ish on the interpreters — fine for cookies and etags, not meant for bulk hashing. Coverage: oracle, IR, emit-C++, LLVM — not frozen ELF, which never gained the post-freeze byteAt/byteToString primitives these lean on engines.

namespace math

A small, mostly-native namespace for constants and transcendentals:

namespace math {
    const float pi = 3.141592653589793;
    const float e  = 2.718281828459045;
    float log(float x);   float log2(float x);   float exp(float x);
    float sin(float x);   float cos(float x);    float tan(float x);
    float atan2(float y, float x);
    int   min(int a, int b);      int   max(int a, int b);
    float min(float a, float b);  float max(float a, float b);
}

Accessed as math::pi, math::log(x), and so on, or via uses math; for bare pi/log(x) in scope (see Namespaces & Injection). min/max are ordinary in-language functions that overload by argument type within the one namespace — an int pair and a float pair coexist. Every other member is a native free function backed by libm.

Not std::math

math is a top-level namespace, not nested under std — a qualified std::math::pi path is a real gap, not a style choice. math is the design's own pre-registered fallback for exactly that failure mode.

engines pi/e/min/max run everywhere. The transcendentals (log, log2, exp, sin, cos, tan, atan2) are native libm/LLVM-intrinsic calls, covered on the oracle, IR, emit-C++, and LLVM backends; on the frozen ELF backend they fail with a clean coverage diagnostic instead of linking libm into that zero-dependency backend or silently misbehaving.

Block: the byte buffer

Block is the language's gated mutable byte buffer — the gate is the type itself, nothing implicit converts to or from it. Unlike the pure/immutable Array/Map, Block is a reference type: honestly mutable, shared by reference like any class.

Block b = Block(4096);              // zeroed, fixed length; b.length() == 4096
Block c = Block::fromString(str);   // copy a string's bytes into a new Block

int  v  = b.byteAt(i);              // 0..255
b.setByte(i, v);                    // v must be 0..255 (else throws — not masked)

Block  s = b.slice(off, len);       // ALIASING view (shares bytes with b)
string t = b.toString(off, len);    // copy len bytes -> string

int u = b.int32At(i); b.setInt32(i, u);   // little-endian; read sign-extends
int w = b.int64At(i); b.setInt64(i, w);   // little-endian

b.fill(off, len, v);                // native bulk byte fill
b.blit(dstOff, src, srcOff, len);   // native copy; memmove overlap semantics

bool same  = b.equals(c);           // CONTENT equality (unlike `==`)
int  first = b.mismatch(c, from);   // first differing index, or -1
methodbehavior
Block(int size)construct a zeroed buffer of size bytes (fixed length)
Block::fromString(string s)new Block copying s's bytes (labeled constructor)
length() -> intbyte length
byteAt(int i) -> intbyte 0..255 at i
setByte(int i, int value)store a byte; value outside 0..255 throws (loud, not masked)
slice(int off, int len) -> Blockaliasing view sharing storage — writes through the slice are visible in the parent (zero-copy, by design)
toString(int off, int len) -> stringcopy len bytes from off into a new string
int32At(int i) -> int / setInt32(int i, int value)little-endian 4-byte read (sign-extended) / write
int64At(int i) -> int / setInt64(int i, int value)little-endian 8-byte read / write
fill(int off, int len, int value)fill [off, off+len) with one byte; value must be 0..255
blit(int dstOff, Block src, int srcOff, int len)copy bytes with memmove semantics, including overlapping views of one root
equals(Block other) -> boolcompare full-view content; unequal lengths return false
mismatch(Block other, int from) -> intfirst differing index at or after from, else -1; unequal lengths throw

Every access is bounds-checked and throws RuntimeException on an out-of-range offset or length — including setByte's 0..255 value range. Empty bulk ranges are legal at the inclusive end offset; mismatch accepts from == length() and returns -1.

Two deliberate meanings of equality

b.equals(c) compares byte content, while b == c remains ordinary Block reference identity. Neither operation changes the other. console.write(b) renders Block(len=N), not the bytes themselves.

engines Block ships on the oracle, IR, emit-C++, and LLVM engines as a heap value (tag 11, body {parentPtr, off, len, dataPtr}) whose slice retains the root so shared bytes outlive any single view. There is no ELF lane — the frozen X64Gen backend never gained the ABI addendum Block needs, so Block-typed I/O and Block itself are absent there.