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
Note: The pre-built binary is for Linux x86-64. To compile from source, ensure LLVM 21.1 is installed as a system library.

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.

FlagDescription
-o <file>Output file path
--target wasm32Compile to WebAssembly
--bootstrapSelf-hosting bootstrap mode
--helpPrint 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

TypeDescriptionExample
i64Signed 64-bit integer42, -1, 0
f64Double-precision floating point3.14, -0.5
boolBooleantrue, false
strUTF-8 string (immutable, owned)"hello"

Compound Types

TypeDescriptionExample
[T; N]Fixed-size array[1, 2, 3]
(T, U)Tuple(42, "hello")
fn(T) -> UFunction pointer typefn(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.

Tip: If your generic function requires ordering, constrain with 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.

FunctionDescription
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.

MethodDescription
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.

MethodDescription
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_string
fs_write
fs_create_dir
fs_create_dir_all
fs_remove_file
fs_remove_dir
fs_rename
fs_copy

std::net

Networking primitives for TCP and UDP communication.

TcpListener::bind
TcpStream::connect
read / write
UdpSocket::bind

std::sync

Synchronization primitives for concurrent programming.

atomic_load
atomic_store
atomic_compare_exchange
Mutex

std::time

Time measurement and duration types.

Duration::from_secs
Duration::from_millis
SystemTime::now

std::iter

Iterator traits and adapter combinators.

Iterator::map
Iterator::filter
Iterator::fold
Iterator::sum
Iterator::collect

Compile-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.

Tip: Use 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;
}
Performance: 100,000 actors exchanging ping-pong messages completes in 0.94 ms — 50% faster than Rust's standard channels.

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.

RangeCategory
E1001–E1999Lexical / parsing errors
E2001–E2999Type mismatches and inference failures
E3001–E3999Resolution and name binding errors
E4001–E4999Concept satisfaction failures
E5001–E5999MIR lowering errors
E6001–E6999Codegen errors
E7001–E7999CTFE / comptime evaluation errors
E8001–E8999Standard library errors
E9001–E9015Internal 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