LEVIATHAN v0.1 · in development

Declarations

Structs & Enums

Two closed, final value types: struct for data that's copied by value with no identity, and enum for a fixed set of named members carried by an int. Both are what let a dense, unboxed Array<T> exist.

Value structs

A struct is a value type — copied on every bind, pass, return, and store. The copy is deep: a nested struct field copies along with it, though a reference field (a class instance) is shared, not cloned. A struct has no identity — two structs with equal fields are equal, there's no separate notion of "the same instance" — and it is final: it may implement interfaces, but it cannot inherit implementation from another struct, nor be inherited from.

struct Point {
    int x;  int y;
    int dot() => x * x + y * y;
    mutating void translate(int dx, int dy) { x = x + dx; y = y + dy; }
}

Because every assignment of a struct value is a copy, passing a Point into a function and mutating it there never affects the caller's copy — there's nothing to alias.

Mutating methods

Any method that writes to a field of this must be marked mutating. A plain (non-mutating) method that assigns a field is a compile error — it's the compiler's way of making the mutation visible at the call site's declaration, not just inside the method body. Constructors and set accessors are mutating by definition and don't need the keyword written out.

struct Point {
    int x;  int y;

    // OK — declared mutating, writes a field
    mutating void translate(int dx, int dy) { x = x + dx; y = y + dy; }

    // compile error — writes `x` without `mutating`
    void reset() { x = 0; }
}
struct vs. class

Reference class is unchanged by any of this — struct and class sit side by side. Pick struct for data: rows, coordinates, small immutable-feeling bundles that should copy cleanly and compare by value. Pick class for entities: things with identity, shared mutable state, or an inheritance hierarchy.

Enums

An enum is also a value type — copied, no identity, final — with a closed member set carried by an int. Members without an explicit value auto-increment from the previous member's carrier; members after an explicit value continue from it. Duplicate carrier values across members are a compile error.

enum Method { GET, HEAD, POST }                    // carrier: int, auto 0..n-1
enum Status : int { OK = 200, NotFound = 404, Teapot = 418 }   // explicit carriers
enum Gap : int { A, B = 10, C }                     // auto-after-explicit: A=0, B=10, C=11

Members live on the static side of the type, reached with :: rather than . — the same non-instantiated side documented in Members & Accessors. A bare declaration with no initializer takes the first-declared member.

Method m = Method::GET;              // member access
m.code()                             // 0     — carrier value (int)
m.toString()                         // "GET" — member name
Method::fromCode(200)                // Method? — None if no member's carrier matches
Method d;                            // bare declaration -> the first-declared member (GET)
MemberSignatureMeaning
code()() -> intthe carrier value backing this member
toString()() -> stringthe member's declared name
fromCode(int)(int) -> Enum?static — looks up a member by carrier, None if no member matches

: int is the only carrier in v1 — string carriers are deferred. An enum's ::-member access and match arms resolve through two narrow rules gated on the enum's registration: under the hood, an enum desugars to a value struct holding a single int code field, plus one compiler-mangled constant global per member and a generated fromCode free function. No new value kind and no ABI tag are introduced, which is why enum has full coverage on every native backend, LLVM included.

Enum operators & match

== / !=, and ordering with <, compare enum values by their carrier. As a Map/Set key, an enum also compares by that same carrier value.

bool same   = (Method::GET == Method::GET);   // true — same carrier
bool ranked = (Status::OK < Status::NotFound); // true — 200 < 404

match over an enum is exhaustive over the closed member set: cover every member and no else arm is needed. Omit one and it's a compile error that names the missing member(s) — the set can never silently grow stale. An else arm is still allowed when you don't want to spell out every case.

match (m) { Method::GET => ...; Method::HEAD => ...; Method::POST => ...; }  // exhaustive
match (s) { Status::OK => "ok"; else => "other"; }                          // else allowed

Columnar storage engines

An Array<T> whose element type T is a columnar-eligible value struct is stored column-major (struct-of-arrays) on the native backends instead of row-major: all the first fields contiguously, then all the second fields, and so on — one refcounted allocation with a header and per-field column sections, tag-free 8-byte payload columns.

No user-visible surface

There is no new type, annotation, or syntax to opt in. Array<T> stays the same pure value with the same method surface — only the physical layout changes, and value semantics make the difference unobservable from Leviathan code.

Eligibility. T is columnar iff it's a value struct with at least one field and every field is a plain scalar — int, float, bool, or char — with no weak, optional (T?), union, nested-struct, or reference field. A struct that fails this (say, one with a string field) keeps the row-major dense layout permanently. An empty array is boxed and flips to columnar on its first eligible-struct append.

The win. A direct-index scan of one field — for (int i in 0..n-1) s = s + arr[i].hot; — fuses to a single column read that touches only that field's contiguous memory, no per-element gather. Measured about 4.5× faster than row-major on an 8-int struct, plus roughly a smaller footprint across access patterns. The gain grows with struct width.

Where it does not win. Whole-struct consumption — for (T x in arr) — and closure pipelines (map/where/reduce) still gather a fresh standalone struct per element, because value semantics require the copy; that gather allocates, so those patterns are actually slower than the row-major buffer-alias (roughly 1.8× on a narrow struct). Prefer direct-index field access for the locality win; fusing for-in and pipeline access is a designated follow-up, not yet in v1.