Type System

Built-in types, aliases, arrays, nullable types, references, pointers, callables, and error unions.

Zynx is statically typed. The compiler checks type shapes before LLVM lowering, including array sizes, callable signatures, nullable values, reference and raw pointer use, and error-union flow.

Types are written at declaration boundaries, function signatures, casts, and places where inference needs a target:

let count: i32 = 4;
let label: str = "ready";
let values: [i32, 3] = [1, 2, 3];

Built-in Types

GroupTypes
Signed integersi8, i16, i32, i64, i128, isize
Unsigned integersu8, u16, u32, u64, u128, usize
Floating pointf16, f32, f64, f128
Other built-insbool, str, void

Use explicit casts when crossing numeric or distinct-type boundaries:

let small: u8 = 42;
let count: usize = small as usize;

void is not a general value type. Use it as a function return type, as void throws(ErrorSet), or as a raw pointer target such as *void.

const has two meanings. A declaration such as const name = value creates an immutable binding. A type prefix such as const &T creates a read-only view. The type prefix is valid only on checked references, raw pointers, str, and array or slice views:

const &T
const *T
const str
const [T]

Plain value forms such as const i32, const String, const (A, B), const vector<T, N>, and const Future<T> are invalid type forms.

Aliases And Distinct Types

type Size = usize;
type Vec4<T> = [T, 4];
type Fixed<T, N: usize> = [T, N];

fn sum(xs: Vec4<i32>) -> i32 {
    return xs[0] + xs[1] + xs[2] + xs[3];
}

type Name = T creates a transparent alias. The alias keeps signatures readable without creating a separate runtime representation.

Generic aliases can take both type parameters and const integer parameters. Const arguments are written in normal generic argument lists, for example Fixed<i32, 3>. See Generics.

type Name = distinct T creates a new type that does not assign to or from the base type without an explicit cast.

type UserID = distinct u64;

fn main() {
    let id: UserID = 42 as UserID;
    let raw: u64 = id as u64;
    _ = raw;
}

Arrays And Slices

[T, N] is a fixed-size array type. The length N is part of the type, so the compiler checks literals and assignments against that exact size.

fn pair_sum(values: [i32, 2]) -> i32 {
    return values[0] + values[1];
}

fn main() {
    let scores: [i32, 3] = [10, 20, 30];
    let pair = pair_sum([1, 2]);

    assert scores.length == 3;
    assert scores[0] == 10;
    assert pair == 3;
}

[T] is a slice-shaped array value. It carries a pointer and length, supports .length, .ptr, indexing, equality for comparable element types, and iteration.

fn sum(values: const &[i32]) -> i32 {
    return values[0] + values[1] + values[2];
}

fn main() {
    let values: [i32] = [1, 2, 3];

    assert values.length == 3;
    assert sum(&values) == 6;

    for value in values {
        _ = value;
    }
}

Array and slice values are move-checked. Passing a [T] by value consumes that slice handle. Use const &[T] when a helper should borrow the slice and the caller should keep using it.

Fixed-size arrays can be converted to [T] slice values:

fn main() {
    let fixed: [i32, 3] = [4, 5, 6];
    let view: [i32] = fixed;

    assert view.length == 3;
    assert view[2] == 6;
}

Put prefix modifiers inside the element position when the array stores that kind of element:

fn first_ref(values: const &[&i32]) -> i32 {
    return *values[0];
}

fn main() {
    let a = 10;
    let b = 20;
    let refs: [&i32] = [&a, &b];

    assert first_ref(&refs) == 10;
    assert *refs[1] == 20;
}

&[T] means a reference to a slice-shaped array. [&T] means a slice-shaped array of references. These forms compose, so &[&T] is a reference to a slice-shaped array of references.

Empty array literals need an expected element type. Array and slice indexing is bounds-checked at runtime.

Nullable Values

T? means a value can be null. Compare nullable values with null before using the value-sensitive path.

fn positive(value: i32) -> i32? {
    if value > 0 {
        return value;
    }
    return null;
}

fn main() {
    assert positive(7) != null;
    assert positive(-1) == null;
}

Use ?. for nullable member access. See Optional Chaining.

Error Unions

T throws(ErrorSet) means a function or expression can produce either a T or an error from ErrorSet. Multiple error sets are written as an explicit union, such as T throws(IOError | ParseError).

error ParseError {
    Empty
}

fn parse(text: str) throws(ParseError) -> i32 {
    if text == "" {
        throw ParseError.Empty;
    }
    return 42;
}

Use try or catch to work with error unions. Error-union values are not valid if or ternary conditions. See Error Handling.

References And Pointers

&T is a reference. const &T is a read-only reference. *T is a raw pointer, and const *T is a read-only raw pointer.

fn add_one(value: &i32) {
    *value = *value + 1;
}

fn main() {
    let count = 1;
    add_one(&count);
    assert count == 2;
}

References are the normal borrowing tool. Raw pointers are lower-level interop values. Prefer references unless a standard-library or native boundary expects a pointer. See Memory Model for borrow scopes, exclusive mutable borrows, and raw pointer caveats.

Unsafe operations such as raw pointer dereference, pointer arithmetic, pointer/integer casts, foreign calls, inline assembly, volatile access, and runtime intrinsics require an unsafe {} block or an unsafe caller boundary. Unsafe context is permission for those operations; it does not disable ordinary checks.

Raw pointers are already nullable, so use null directly with *T:

fn main() {
    let value: u32 = 10;
    let pointer: *u32 = &value as *u32;

    unsafe {
        *pointer = 42;
    }

    let none: *u32 = null;
    assert pointer != none;
}

Tuples And Callables

Tuple types use parentheses and comma-separated member types. Tuple values are indexed with [] and can be destructured.

fn pair() -> (i32, str) {
    return (7, "ok");
}

fn main() {
    let (code, text) = pair();
    assert code == 7;
    assert text == "ok";
}

Callable types use the call signature syntax (ArgTypes...) -> ReturnType.

fn apply(value: i32, f: (i32) -> i32) -> i32 {
    return f(value);
}

fn main() {
    let out = apply(21, (x) => x * 2);
    assert out == 42;
}

See Closures and Captures for closure literals and capture rules.

Ownership Types

Unique<T> is a compiler-recognized owned value type used by ownership and return-promotion flows. Owned values are move-checked: assigning or passing them can transfer ownership instead of copying.

See Move, Copy, and Clone for the ownership rules and Memory Model for borrowing, Unique<T>, and escaping local values.

SIMD Vectors

vector<T, N> is a fixed-size numeric vector type for low-level SIMD-style work.

fn main() {
    let a: vector<i32, 4> = [1, 2, 3, 4];
    let b: vector<i32, 4> = [10, 20, 30, 40];
    let c = a + b;
    let first: i32 = c[0];
    assert first == 11;
}

Vector element types must be integer or floating-point scalars, and the lane count must be a positive integer literal. See SIMD Vectors.

Type Forms

FormMeaning
Name<T>named generic type
Name<T, 3>named generic type with a const integer argument
[T, N]fixed-size array
[T]unsized array/slice-shaped type
T?nullable type
T throws(E)error-union type
&Treference
*Tpointer
const &T, const *T, const [T]read-only view forms
Unique<T>owned value type
distinct Tdistinct alias target
(A, B)tuple type
(A) -> Bcallable type
vector<T, N>fixed-size numeric vector