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; }
}
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)| Member | Signature | Meaning |
|---|---|---|
code() | () -> int | the carrier value backing this member |
toString() | () -> string | the 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 allowedColumnar 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.
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
2× 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.