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];
| Group | Types |
|---|---|
| Signed integers | i8, i16, i32, i64, i128, isize |
| Unsigned integers | u8, u16, u32, u64, u128, usize |
| Floating point | f16, f32, f64, f128 |
| Other built-ins | bool, 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.
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;
}
[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.
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.
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.
&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;
}
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.
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.
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.
| Form | Meaning |
|---|---|
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 |
&T | reference |
*T | pointer |
const &T, const *T, const [T] | read-only view forms |
Unique<T> | owned value type |
distinct T | distinct alias target |
(A, B) | tuple type |
(A) -> B | callable type |
vector<T, N> | fixed-size numeric vector |