Installation
Zeta v1.0.1 ships as a single static binary with zero dependencies. Clone the repository and the compiler is ready to go:
git clone https://github.com/murphsicles/zeta.git
cd zeta
./bin/zetac --help
Once you have the compiler, you can verify it works:
./bin/zetac examples/hello.z -o hello
./hello
# Output: Hello, Zeta!
Hello World
Every Zeta program begins with a main function. It returns i64 (the exit code) and is the entry point the runtime calls.
fn main() -> i64 {
println_str("Hello, Zeta!");
return 0;
}
println_str prints a string to stdout. Zeta provides several built-in print functions: println_str, println_i64, println_f64, println_bool.
Save to hello.z, compile with ./bin/zetac hello.z -o hello, and run with ./hello.
Compilation
The Zeta compiler accepts source files and produces native executables or WebAssembly modules.
| Flag | Description |
|---|---|
-o <file> | Output file path |
--target wasm32 | Compile to WebAssembly |
--bootstrap | Self-hosting bootstrap mode |
--help | Print usage information |
# Compile to native binary
./bin/zetac hello.z -o hello
# Compile to WebAssembly
./bin/zetac --target wasm32 hello.z -o hello.wasm
# Self-hosting bootstrap (compile the compiler)
./bin/zetac src/main.z -o zetac
The compiler itself compiles in approximately 14 ms on modern hardware. Output binaries are typically a few kilobytes.
Comments
Zeta uses C-style line comments. There are no block comments.
// This is a single-line comment
let x = 42; // inline comment
// Comments can span multiple lines
// like this. There is no /* ... */ syntax.
Variables
let bindings
Variables are introduced with let. They are immutable by default — once bound, they cannot be reassigned.
let x = 42; // immutable i64
x = 43; // ❌ compiler error — x is immutable
Mutability
Use mut to make a variable mutable:
let mut counter = 0;
counter += 1; // ✅ allowed
counter = 100; // ✅ allowed
Type Inference
Zeta infers types from the assigned value. Explicit type annotations can override inference:
let a = 42; // inferred i64
let b = 3.14; // inferred f64
let c: i64 = 99; // explicit i64
let d: f64 = 42; // i64 literal promoted to f64
Constants
const declares a compile-time constant. The value must be determinable at compile time.
const MAX_COUNT: i64 = 1000;
const NAME = "Zeta"; // type inferred
Types
Primitive Types
| Type | Description | Example |
|---|---|---|
i64 | Signed 64-bit integer | 42, -1, 0 |
f64 | Double-precision floating point | 3.14, -0.5 |
bool | Boolean | true, false |
str | UTF-8 string (immutable, owned) | "hello" |
Compound Types
| Type | Description | Example |
|---|---|---|
[T; N] | Fixed-size array | [1, 2, 3] |
(T, U) | Tuple | (42, "hello") |
fn(T) -> U | Function pointer type | fn(i64) -> i64 |
Type Casting
Use the as keyword for explicit type conversion:
let x: f64 = 42; // i64 → f64
let y = (x * 2.0) as i64; // f64 → i64
Functions
Named Functions
Functions are declared with fn, followed by the name, parameters, return type, and body.
fn add(a: i64, b: i64) -> i64 {
return a + b;
}
Return Values
Use return to exit early, or let the last expression serve as the return value:
fn square(x: i64) -> i64 {
x * x // last expression is the return value
}
fn abs(x: i64) -> i64 {
if x >= 0 {
return x; // early return
}
-x
}
First-Class Functions
Functions are values. They can be passed as arguments, returned from other functions, and bound to variables.
fn apply(f: (i64) -> i64, x: i64) -> i64 {
return f(x);
}
let result = apply(square, 5); // 25
Closures
Anonymous functions capture variables from their enclosing scope. The syntax is fn(params) { body }.
fn make_adder(n: i64) -> (i64) -> i64 {
return fn(x: i64) -> i64 {
x + n // n is captured from the enclosing scope
};
}
let add5 = make_adder(5);
let y = add5(10); // 15
Control Flow
if / else
if is an expression — it produces a value that can be bound.
let max = if a > b { a } else { b };
if x < 0 {
println_str("negative");
} else if x == 0 {
println_str("zero");
} else {
println_str("positive");
}
While Loops
let mut i = 0;
while i < 10 {
println_i64(i);
i += 1;
}
For Loops
The for loop iterates over ranges and collections.
// Range iteration
for i in 0..10 {
println_i64(i);
}
// Collection iteration (Vec, array, etc.)
let v = Vec::new();
v.push(1); v.push(2); v.push(3);
for item in v {
println_i64(item);
}
Match
match is a powerful pattern-matching expression. It must be exhaustive — all possible cases must be handled.
fn describe(val: i64) -> str {
return match val {
0 => "zero",
1 => "one",
_ => "other", // wildcard catch-all
};
}
// Destructuring enums
match opt {
Option::Some(n) => process(n),
Option::None => 0,
}
Structs
Definition
struct Point {
x: i64,
y: i64,
}
Construction
let p = Point { x: 3, y: 4 };
// Shorthand field init (variable name matches field name)
let x = 10;
let y = 20;
let q = Point { x, y };
Field Access
let px = p.x;
let py = p.y;
Methods via impl
impl Point {
fn origin() -> Point {
return Point { x: 0, y: 0 };
}
fn magnitude(self) -> f64 {
return sqrt((self.x * self.x + self.y * self.y) as f64);
}
}
let p = Point::origin();
let m = p.magnitude(); // 0.0
Enums
Definition
enum Option<T> {
Some(T),
None,
}
enum Color {
Red,
Green,
Blue,
}
Usage
let present = Option::Some(42);
let absent = Option::None;
let c = Color::Red;
Pattern Matching on Enums
fn unwrap_or<T>(opt: Option<T>, default: T) -> T {
return match opt {
Option::Some(val) => val,
Option::None => default,
};
}
Tuples
Tuples group multiple values of possibly different types. They are indexed by position with dot notation.
let pair = (42, "hello");
let num = pair.0; // 42
let msg = pair.1; // "hello"
// Tuple types: (i64, str)
fn split() -> (i64, str) {
return (1, "one");
}
Arrays & Slices
Fixed-Size Arrays
Arrays have a fixed length determined at compile time. They live on the stack by default.
let arr: [i64; 5] = [1, 2, 3, 4, 5];
let first = arr[0]; // index access
// Arrays can be iterated
for val in arr {
println_i64(val);
}
Vector (Dynamic Array)
Vec provides a dynamically-sized, heap-allocated array. It is part of the standard library (std::vec).
let v = Vec::new();
v.push(10);
v.push(20);
v.push(30);
let first = v[0]; // 10
let len = v.len(); // 3
Generics
Zeta supports parametric polymorphism with type parameters in angle brackets. Generics are monomorphized — each concrete instantiation generates specialized code.
fn identity<T>(x: T) -> T {
return x;
}
let a = identity(42); // T inferred as i64
let b = identity("hi"); // T inferred as str
Generic Structs
struct Pair<A, B> {
first: A,
second: B,
}
let p = Pair { first: 1, second: "two" };
Concepts
Concepts constrain type parameters, similar to Rust's traits or Haskell's type classes. They describe what operations a type must support.
fn max<T: Ord>(a: T, b: T) -> T {
if a >= b { return a; }
return b;
}
Concept Refinement Hierarchy
Inspired by Stepanov's Elements of Programming, concepts form a refinement lattice:
Regular → TotallyOrdered → Semigroup → Monoid → Group → Ring
A type satisfying a higher-level concept automatically satisfies all its parents. This enables writing generic algorithms that are provably correct at the type level.
Ord. If it requires addition with identity, use Monoid. The compiler checks concept satisfaction at compile time.
Memory Model
Stack Priority
Zeta allocates on the stack by default. Fixed-size arrays, structs, and tuples live on the stack unless explicitly moved to the heap.
Heap Allocation
Use Vec, String, or other standard library types for heap allocation:
let v = Vec::new(); // heap-allocated buffer
v.push(1);
v.push(2);
Ownership
Zeta uses a straightforward ownership model: one writer or many readers at any point. This is tracked at compile time without explicit lifetimes or a traditional borrow checker.
let data = Vec::new();
data.push(1);
let len = data.len(); // immutable read — multiple allowed
let first = data[0]; // immutable read
data.push(2); // mutable write — exclusive access
Correctness
Preconditions
pre asserts a condition at function entry. If the condition fails at runtime, the program halts with a descriptive message.
fn divide(a: i64, b: i64) -> i64 {
pre(b != 0, "division by zero");
return a / b;
}
Postconditions
post asserts a condition on the return value before the function exits.
fn sqrt_safe(x: f64) -> f64 {
pre(x >= 0.0, "sqrt of negative number");
let result = sqrt(x);
post(result >= 0.0, "result must be non-negative");
return result;
}
Loop Invariants
invariant asserts a property that must hold at each iteration of a loop.
let mut sum = 0;
for i in 0..n {
invariant(sum == i * (i - 1) / 2);
sum += i;
}
Attributes
Attributes decorate types and functions with metadata. They use #[...] syntax.
// Mathematical property annotations
#[commutative]
fn add(a: i64, b: i64) -> i64 { a + b }
#[associative]
fn multiply(a: i64, b: i64) -> i64 { a * b }
#[identity]
fn zero() -> i64 { 0 }
These annotations enable algebraic optimizations during CTFE — the compiler can reorder, fuse, or eliminate operations based on declared properties.
Standard Library
Zeta v1.0.1 ships with 20+ standard library modules organized into three tiers. All modules are written in self-hosted Zeta.
std::mem
Memory introspection and manipulation primitives.
| Function | Description |
|---|---|
size_of<T>() | Returns the size of type T in bytes |
align_of<T>() | Returns the alignment of type T |
swap(a, b) | Swaps two values |
replace(dest, src) | Replaces dest with src, returns old value |
let s = size_of<i64>(); // 8
let a = align_of<i64>(); // 8
std::vec
Dynamic, heap-allocated vector with amortized O(1) push.
| Method | Description |
|---|---|
Vec::new() | Create a new empty vector |
push(val) | Append an element |
len() | Number of elements |
v[i] | Index access (zero-based) |
pop() | Remove and return the last element |
let v = Vec::new();
v.push(10);
v.push(20);
let sum = v[0] + v[1]; // 30
std::string
UTF-8 encoded string type with methods for inspection and conversion.
| Method | Description |
|---|---|
len() | Returns the number of bytes (not characters) |
contains(substr) | Checks if the string contains a substring |
starts_with(prefix) | Checks the prefix |
ends_with(suffix) | Checks the suffix |
std::fs
Filesystem operations backed by the host OS.
fs_read_to_stringfs_writefs_create_dirfs_create_dir_allfs_remove_filefs_remove_dirfs_renamefs_copystd::net
Networking primitives for TCP and UDP communication.
TcpListener::bindTcpStream::connectread / writeUdpSocket::bindstd::sync
Synchronization primitives for concurrent programming.
atomic_loadatomic_storeatomic_compare_exchangeMutexstd::time
Time measurement and duration types.
Duration::from_secsDuration::from_millisSystemTime::nowstd::iter
Iterator traits and adapter combinators.
Iterator::mapIterator::filterIterator::foldIterator::sumIterator::collectCompile-Time Evaluation (CTFE)
comptime blocks execute arbitrary Zeta code during compilation. The result is baked into the binary — zero runtime cost.
const FACTORIAL_10: i64 = comptime {
fn fact(n: i64) -> i64 {
if n <= 1 { return 1; }
return n * fact(n - 1);
}
fact(10)
};
// FACTORIAL_10 = 3628800 — computed at compile time
CTFE supports the full Zeta language: functions, loops, conditionals, variables, and more. The compiler evaluates comptime blocks using the same MIR interpreter used for constant folding and algebraic fusion.
comptime for lookup tables, precomputed constants, code generation, and any computation that can be moved from runtime to compile time.
WebAssembly
Zeta compiles natively to WebAssembly. Pass --target wasm32 to the compiler and get a .wasm module that runs in any Wasm runtime or browser.
fn main() -> i64 {
println_str("Hello from the browser!");
return 0;
}
// Compile:
// ./bin/zetac --target wasm32 hello.z -o hello.wasm
// Run:
// wasmtime hello.wasm
WASM modules are compiled through the same LLVM pipeline. Output is typically ~4 kB with zero runtime overhead.
SIMD & Auto-Vectorization
Zeta's type system enables aggressive SIMD optimization. The CacheSafe alias analysis guarantees that LLVM can auto-vectorize loops without manual hints or unsafe blocks.
fn vector_add(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
return a + b; // compiles to SIMD instructions
}
fn sum_array(arr: [f64; 1024]) -> f64 {
let mut sum: f64 = 0.0;
for i in 0..1024 {
sum += arr[i];
}
return sum;
// auto-vectorized to SIMD reduction
}
Actors & Concurrency
Zeta provides a lightweight actor model with M:N threading. Actors communicate via typed channels. The work-stealing scheduler distributes actors across available OS threads.
fn main() -> i64 {
let (tx, rx) = channel<str>();
spawn(fn() {
tx.send("ping");
});
let msg = rx.recv();
println_str(msg); // "ping"
return 0;
}
Foreign Function Interface
Zeta can call C functions through extern "C" blocks, declared via std::ffi.
// Call a C function from Zeta
extern "C" fn puts(s: *str) -> i64;
fn main() -> i64 {
puts("Hello from Zeta FFI!");
return 0;
}
Murphy's Sieve
Competition-ready prime counting using a 30030-wheel (2×3×5×7×11×13) for 80.8% reduction in checks. Baked into Zeta's standard library and test suite.
fn murphy_sieve(limit: i64) -> i64 {
if limit < 2 { return 0; }
const WHEEL: i64 = 30030; // product of first 6 primes
let mut count: i64 = 1; // 2 counts as prime
let mut i: i64 = 3;
while i <= limit {
if is_coprime_to_wheel(i) {
if is_prime(i) { count += 1; }
}
i += 2;
}
return count;
}
See docs/02_MURPHYS_SIEVE_IMPLEMENTATION_GUIDE.md in the repository for the full treatment.
Compiler Pipeline
The Zeta compiler transforms source code through multiple intermediate representations:
Zeta Source (.z)
│
▼ Lexer → Parser
│ Tokenizes source and builds the AST
│
▼ HIR (High-level IR)
│ Resolves imports, identities, and macros
│
▼ Resolver
│ Type resolution, generics, specialization, concept checking
│
▼ THIR (Typed HIR)
│ Fully resolved types and identities
│
▼ MIR (Mid-level IR)
│ Control flow graph with basic blocks — optimizations applied here
│
▼ LLVM Codegen
│ Lowers MIR to LLVM IR, runs LLVM optimization passes
│
▼ Machine Code
│ Native executable or WebAssembly module
The compiler is itself written in Zeta. Every stage of this pipeline is compiled by Zeta, proving the bootstrap loop is closed.
Error Codes
Zeta has 175+ distinct error codes, each with a unique identifier (E1001–E9015). Every error tells you what went wrong, where, and how to fix it.
| Range | Category |
|---|---|
| E1001–E1999 | Lexical / parsing errors |
| E2001–E2999 | Type mismatches and inference failures |
| E3001–E3999 | Resolution and name binding errors |
| E4001–E4999 | Concept satisfaction failures |
| E5001–E5999 | MIR lowering errors |
| E6001–E6999 | Codegen errors |
| E7001–E7999 | CTFE / comptime evaluation errors |
| E8001–E8999 | Standard library errors |
| E9001–E9015 | Internal compiler errors |
// Example error output:
error[E2001]: type mismatch: expected i64, found str
┌─ main.z:3:17
│
3 │ let x: i64 = "hello";
│ ^^^^^^^
│ expected i64, found str
│ use explicit conversion or change the type annotation
This documentation covers Zeta v1.0.1 — the foundational release.
Try Zeta in the Playground ·
Take the Tour