| Category | Types |
|---|---|
| Unsigned integers | u8, u16, u32, u64, u128 |
| Signed integers | i8, i16, i32, i64, i128 |
| Floats | f32, f64 |
| Other primitives | bool, char, void |
| Compiler-internal | unknown — a valid but unresolved type, not user-instantiable. invalid — a dead or uninitialized state (a moved variable, a proven-null variable). Neither can be named or stored by user code. |
| Special | never, null |
| Opaque | opaque — a reference type (view by default), equivalent to a tagged void*. raw opaque is an untagged void*, used for C interop. |
| Syntax | Meaning |
|---|---|
[]T |
Slice — a fat pointer (pointer + length). String literals are []char. |
[N]T |
Fixed-size array, stack-allocated. |
[_]T |
Deduced-size array — the compiler infers N from the initializer. |
char is a UTF-8 code unit. string is a built-in library type backed by []char with UTF-8 encoding.
NRA tracks the origin of every string node — literal, allocator, or local (see §7.1 for the complete set):
// []char implicitly casts to string, zero-cost (literal origin)
let s: string = "hello";
// Concatenation changes origin to allocator -- triggers allocation
let greeting = "hello" + " " + "world";
A closed set of named constants. All values must be known at compile time. Zith supports three styles:
enum Direction { North, South, East, West }
enum Status: i32 { Ok = 0, Err = 1, Pending = 2 }
enum Color: rgb {
Red = { r: 255, g: 50, b: 0, a: 255 },
Green = { 0, 255, 0, 255 },
}
Use individual fields for unrelated fields, and [] groups for semantically related fields that share a type:
struct Sample { name: string, age: i32 }
struct Point { [x, y, z]: f32 }
struct Transform {
[x, y, z]: f32, // position
[rx, ry, rz]: f32, // rotation
}
struct Pair<T, U> { first: T, second: U }
// Doubly-linked list node
struct Node<T> {
data: T,
//Self = Node<T>
next: ?unique Self, // owns next; null at tail
prev: ?belong Self, // back-ref; lifetime tied to parent; null at head
}
Structs, enums, and unions can declare methods without bodies in the type definition. The implementation goes in an implement block:
// Struct — declares methods, no body
struct Node<T> {
data: T,
next: ?unique Self,
prev: ?belong Self,
fn isHead(self): bool; // declared, no body
fn isTail(self): bool; // declared, no body
}
// Implementation provides the bodies
implement Node<T> {
fn isHead(self): bool { self.prev is null }
fn isTail(self): bool { self.next is null }
}
implement Node<T> as Printable {
fn print(self) { @println("Node({self.data})"); }
}
// Specific implementation for a concrete type
implement Node<f32> { ... }
Components define method bodies inline — they cannot use implement:
component Vec2 {
[x, y]: f32,
fn length(self): f32 { sqrt(self.x*self.x + self.y*self.y) }
fn dot(self, other: Self): f32 {
self.x*other.x + self.y*other.y
}
}
Structs, enums, and unions declare methods (no body) and define them in
implement. Components define methods inline and cannot useimplement.
self, other, Selfself is the current instance. other is a shorthand for a second instance parameter. Self (capitalized) refers to the concrete type currently being implemented.
A plain-old-data (POD) struct, copy by default alongside primitives. Components cannot implement traits, and any inline functions are limited to pure transformations.
component Vec2 {
[x, y]: f32,
fn length(self): f32 { sqrt(self.x*self.x + self.y*self.y) }
fn dot(self, other: Self): f32 {
self.x*other.x + self.y*other.y
}
}
A component must satisfy all of the following constraints:
component Name: Traits is not allowed).void is not allowed.?unique Self, ?belong Self).By default, a union is runtime-tagged, with variants separated by commas:
union Value { i32, f64, bool }
enum Flag { A, B, C }
let flag = Flag.A;
// Type hint forces union deduction
let x: union = when (flag) {
A = 42,
B = 3.14,
C = true,
};
Without an explicit
uniontype hint, the compiler cannot deduce a union — it must be stated explicitly.dynworks the same way as a type hint — see §14.2.
raw union is an untagged C-style union, valid only inside raw contexts. Accessing the wrong variant is undefined behavior.
Unions can also have named variants, similar to Rust enums:
union Shape {
Circle = { radius: f32 },
Rect = { w: f32, h: f32 },
Point,
}
fn area(s: Shape): f32 {
when (s) {
Circle = s.radius * s.radius * 3.14f,
Rect = s.w * s.h,
Point = 0,
}
}
You can also combine enum with union for compile-time constants that carry data:
enum Constants: union {
pi = 3.14f,
vector = |x: -1, y: 0, z: -1, w: 1|,
nothing = 0,
}
Both
union&enumcan have methods, but neither can implement traits. Pack literals inenum:unionvariants are treated as anonymous structs with a concrete layout.
is) & Flow TypingThe is operator narrows a union within a branch. Branches are isolated — moves inside one branch don't affect others. After the block completes, the type widens back to the full union. The underlying tag does not revert — the value stays i32 internally — but the type system treats it as the full union again.
When you write a conditional as an expression and not every branch returns a value, the missing branches return null. The result becomes ?T:
let result = if (v is i32) v; // ?i32 — missing branch returns null
Using the narrowed value outside the if is a compile error. Recover it by storing the result of the branch expression.
fn handle(v: Val): void {
if (v is i32) {
@println("int: {v}"); // v is i32 here
} else (v is f64) {
@println("float: {v}");
} else {
@println("str: {v}"); // compiler knows v is []char here
}
// v is Val again (full union)
}
// Widening — mutation inside a branch
when (v) {
i32 = { v = 42 }, // v is still i32 internally, but types as Val after
}
// v is Val again here
// when with branch tags
when (v) {
n: i32 = @println("int: {n}"),
f64 = @println("float: {v}"),
_ = @println("other"),
}
when (shape) {
Circle = @println("circle r={shape.radius}"),
Rect = let [val..] = shape, // val = w; remaining fields ignored
_ = @println("other"),
}
// Standalone boolean narrowing & compile-time reflection
// All boolean conditions must be wrapped in parentheses.
let numeric = (v is i32) or (v is f64);
let isStruct = (T is @struct);
// Explicit constraints
fn serialize<T: Serializable + Printable, U: Clone>(val: T, ctx: U): string { ... }
// Implicit constraints inferred from usage at the call site
fn add(a, b) { a + b } // Arithmetic is implicitly required
when — Pattern Matchingwhen (count) {
0 = @println("none"),
1 = @println("one"),
2..=10 = @println("few"),
_ = @println("many"),
}
// As an expression
let label = when (score) {
90..100 = "A",
70..90 = "B",
_ = "C"
};
// '..' ignores the remaining fields
when (point) {
[x..] = @println("x=", x),
_ = @println("no match"),
}
// '..' before a binding captures the last element
when (point) {
[..w] = @println("w={w}"),
_ = @println("no match"),
}
let n: i32 = 42;
let f = n as f64;
Zith Language Specification — Draft v0.9