Skip to content

Vector Types

Vector<T, N> is a fixed-size scalar vector value. It is not an array or slice. It has a stable scalar-array memory layout and a structural C ABI spelling for the C-native lane subset.

Status: current public API unless a rule is marked unsupported or unspecified.

Vectors are useful when a value should carry several scalar lanes with one static type, for example Vector<i32, 4>.

Type Rules

The element type T must be bool, a builtin integer scalar type (including custom iN/uN widths from 1 through 65535 bits), or a builtin floating-point scalar type. A bool-lane Vector is a mask.

Valid examples:

Zynx
let a: Vector<i32, 4> = [1, 2, 3, 4];
let b: Vector<u32, 4> = [1, 3, 7, 15];
let c: Vector<f32, 4> = [1.0, 2.0, 3.0, 4.0];
let mask: Vector<bool, 4> = [true, false, true, false];

Invalid element types include str, references, raw pointers, arrays, slices, nullable types, error-union types, Unique<T>, structs, tuples, closures, Future, Task, and Group.

The lane count is a checked usize shape expression. A literal (4) or const parameter (N) may appear directly; a compound expression must be braced, for example Vector<T, {N + 1}>. Parentheses, integer casts, unary + - ~, and + - * / % << >> & | ^ are supported. Named const aliases are expanded. Overflow, negative results, division by zero, invalid shifts, zero lanes, and more than 65,536 lanes are compile-time errors. A const generic of another integer type must be cast explicitly with as usize, and its value is interpreted at the target's usize width. The total stored object size may not exceed 16 MiB.

Vector dimensions are always static. Runtime-sized tensors, reshaping, and conversions from dynamic slices are separate concepts and are not Vector operations.

Generic signatures may use Vector<T, N> when T is a generic parameter with a Scalar bound (or a more specific Integer/Float bound) and N is a const generic parameter; both are inferred from the argument at the call site:

Zynx
fn double<T: Float, N: usize>(v: Vector<T, N>) -> Vector<T, N> {
	return v + v;
}

std.Vector is the canonical qualified path to the built-in type and works the same as the bare Vector name.

Inside a generic body, a literal length that cannot be proved equal to the symbolic lane count creates a shape obligation. The obligation propagates through generic calls and is checked at each concrete instantiation, with both constraint origins included in a failing diagnostic.

Literals

Vector values use array-literal syntax when the expected type is known, or an explicit constructor:

Zynx
let a: Vector<i32, 4> = [1, 2, 3, 4];
let b = Vector([1, 2, 3, 4]);
let c = Vector<i64>([1, 2, 3, 4]);
let d = Vector<i64, 4>([1, 2, 3, 4]);

An explicitly written element type contextually types unsuffixed elements of a fresh inline literal, so Vector<i64>([1, 2, 3, 4]) constructs i64 lanes from the start. This is not a lane conversion. If the argument is an already typed array value, its element type and length are fixed before construction and must match exactly:

Zynx
let exact: [i64; 4] = [1, 2, 3, 4];
let other: [i32; 4] = [1, 2, 3, 4];

let pass = Vector<i64>(exact);
let fail = Vector<i64>(other); // rejected: i32 is not exactly i64

There is no constructor widening rule or numeric conversion lattice. Use an explicit aggregate as Vector<T, {N}> cast when a typed value's lane type must change. An explicitly written constructor dimension, when present, must also match the argument length exactly.

Vector literals are supported in typed local declarations, function arguments, return expressions, and struct fields.

Zynx
fn first(v: Vector<i32, 4>) -> i32 {
	return v[0];
}

fn values() -> Vector<i32, 4> {
	return [2, 4, 6, 8];
}

struct Box {
	let values: Vector<i32, 4>,
}

fn main() {
	let x = first([5, 6, 7, 8]);
	let y = values();
	let box = Box { values: [3, 6, 9, 12] };
}

The literal length must exactly match the vector lane count.

Without an expected vector type, [1, 2, 3, 4] is an array literal, not a vector literal. There is no implicit array-to-vector conversion.

Zynx
let bad: Vector<i32, 4> = [1, 2, 3];

This reports:

text
vector literal length 3 does not match vector size 4

Operations

Operations work between matching vector types and operate lane-by-lane:

GroupOperatorsOperandsSemantics
Arithmetic+ - * / %matching numeric vectors, or one numeric Vector and a scalar of its exact element type in either orderinteger lanes follow the scalar numeric policy (+/-/* trap on lane overflow; //% trap on lane divide-by-zero or signed min / -1, min % -1); float lanes follow the scalar floating-point policy
Bitwise/shift& | ^ << >>integer vectors onlyshifts discard shifted-out bits; standard signed >> is arithmetic with sign extension and standard unsigned >> is logical with zero fill; each lane's count must be non-negative and less than the element bit width
Comparison== != < <= > >=numeric vectorsreturns Vector<bool, N> lane-by-lane
Mask equality== !=mask vectorsreturns Vector<bool, N> lane-by-lane
Mask logic! & | ^mask vectorseager lane-wise boolean operations

Vector right shift follows scalar semantics independently per lane. For signed i8, i16, i32, i64, i128, and isize elements, >> is equivalent to floor division by 2^count; for unsigned u8, u16, u32, u64, u128, and usize elements, it fills high bits with zero. A negative or width-or-larger runtime count in any lane traps; the same invalid constant lane is rejected at compile time.

Zynx
fn add(a: Vector<i32, 4>, b: Vector<i32, 4>) -> Vector<i32, 4> {
	return a + b;
}

fn mask(a: Vector<u32, 4>, b: Vector<u32, 4>) -> Vector<u32, 4> {
	return (a & b) ^ b;
}
Zynx
let a: Vector<i32, 4> = [1, 2, 3, 4];
let b: Vector<i32, 4> = [10, 20, 30, 40];
let mask = a < b;       // Vector<bool, 4>
let all_ok = @all(mask); // scalar bool

For arithmetic +, -, *, /, and %, numeric vectors accept a scalar operand of exactly the element type in either operand position. The Vector determines the result shape, and the scalar is conceptually splatted to all lanes without materializing a Vector aggregate. Each operand is typed independently; there is no numeric promotion, implicit conversion, coercion from the sibling operand, or coercion from the expected result. Unsuffixed literals keep their normal default type.

Zynx
let a: Vector<i32, 4> = [1, 2, 3, 4];
let b = a + 1;          // default i32 exactly matches the lane type
let factor: i32 = 2;
let c = factor * a;

let wide: Vector<i64, 4> = [1, 2, 3, 4];
let bad = wide + 1;             // rejected: default i32 is not i64
let good = wide + (1 as i64);   // exact i64 scalar is splatted

The left operand is evaluated exactly once before the right operand is evaluated exactly once. Written order is preserved for noncommutative forms: scalar - vector, scalar / vector, and scalar % vector apply that scalar as the left operand of every lane operation. Integer lanes use ordinary checked scalar semantics, including overflow, divide-by-zero, and signed-minimum with -1 traps. Floating-point lanes use ordinary scalar floating-point semantics. Comparisons, bitwise, shift, and mask operators do not accept scalar operands.

Mismatched element types or lane counts are also rejected.

Zynx
let a: Vector<i32, 4> = [1, 2, 3, 4];
let b: Vector<i32, 2> = [5, 6];
let bad = a + b;

This reports:

text
type mismatch in vector arithmetic operation

@all(mask) and @any(mask) accept an exact Vector<bool, N> mask and reduce it to scalar bool. @all is true exactly when every lane is true; @any is true exactly when at least one lane is true. Scalar bool has no identity overload and is used directly, so @all(true) and @any(false) are rejected. Numeric and other Vectors, arrays, and user-defined aggregates are also rejected; there is no shape-polymorphic reduction protocol.

@select(mask, when_true, when_false) requires an exact Vector<bool, N> selector and eagerly selects lane-by-lane. Scalar-bool selectors are rejected. Arguments are evaluated exactly once from left to right. Each branch independently may be an exact Vector<T, N> or scalar exactly T, splatted to all N lanes. Both branches must resolve independently to the same exact T; aggregate/aggregate, aggregate/scalar, scalar/aggregate, and scalar/scalar forms all produce Vector<T, N>. Another aggregate kind or lane count is rejected. Use @all and @any explicitly in conditions because a mask is not a scalar boolean.

Branch typing performs no numeric promotion, implicit conversion, or contextual coercion from the sibling branch or expected result. Literals keep their normal default type, so a Vector<i64, N> branch does not make scalar literal 0 an i64; write 0 as i64 or use a typed local. This branch splat is specific to @select and does not extend Vector mask operators; numeric Vector arithmetic uses the separate exact-element rule above.

Zynx
let values: Vector<i32, 4> = [1, 2, 3, 4];
let zeros: Vector<i32, 4> = [0; 4];
let positive = values > zeros;
assert @all(positive);
let chosen = @select(positive, values, zeros);
let mixed = @select(positive, values, 0);
let filled = @select(positive, 1, 0);

Indexing And Assignment

v[i] returns the vector element type.

Zynx
let v: Vector<i32, 4> = [1, 2, 3, 4];
let first = v[0];

Vector elements can be assigned and used with compound assignment.

Zynx
var v: Vector<i32, 4> = [1, 2, 3, 4];
let i: usize = 2;

v[1] = 9;
v[i] += 5;

Unary + is identity and unary - negates every lane. Integer negation is checked; negating a nonzero unsigned lane or a signed minimum value traps. Masks reject numeric unary operators.

An indexed lane is a projected value and cannot be borrowed: &v[i] is rejected, including when immediately cast to a raw pointer. References to the whole Vector remain supported.

A direct literal out-of-bounds index is a compile-time error. Other index expressions are bounds-checked before the lane is extracted at runtime.

Zynx
let v: Vector<i32, 4> = [1, 2, 3, 4];
let bad = v[4];

This reports:

text
vector index 4 out of bounds (size 4)

Dynamic indexes are checked at runtime. If the index is out of bounds, the program traps with a vector bounds message.

text
Zynx: vector index out of bounds at vector_bounds_check_error.zx:4

Casts, Conversions, And ABI

Vector literals are contextual construction syntax, not an implicit conversion for an array-valued variable. Aggregate as casts are explicit and lane-wise: [S; N] <-> Vector<T, N> and Vector<S, N> -> Vector<T, N> are accepted when each scalar S as T cast is legal and the shapes match. Constructors require an exact element type for a non-literal array argument; use as when the lane type must change.

Aggregate as applies the ordinary checked scalar cast independently to every lane. A lane value outside the destination scalar range traps; precision and rounding follow the scalar cast contract. Shape mismatch is a compile-time error and is never repaired by a cast. Even a widening lane conversion requires the explicit as; no implicit widening exception feeds back into construction or ordinary operators.

Interpolation renders a Vector with brackets and comma-space separators, for example [1, 2, 3]. One scalar format spec is broadcast to every lane: try ^"{v:04d}" can render [0001, 0002, 0003]. Masks render true/false. Every otherwise legal Vector shape is formattable. The Vector expression is evaluated exactly once, then a compact runtime formatter iterates in lane order; there is no per-hole or combined lane cap and no per-lane generated code. Runtime output, allocation, and error cost follow ordinary formatting behavior.

Vector values use LLVM vectors while being computed, but stored values use the canonical layout [N x T]. This implementation detail does not change source semantics. The language guarantees:

  • @sizeof(Vector<T, N>) == N * @sizeof(T);
  • the alignment is the alignment of T;
  • Vector<bool, N> stores one byte per lane. Stores write only 0 or 1, and loads interpret every nonzero byte as true.

The same layout is used for locals, globals, referenced temporaries, and fields inside Zynx structs. Internal function parameters and results may remain vector values in SSA.

At a C ABI boundary, Vector<T, N> corresponds to struct { T lanes[N]; }. The accepted lane spellings are i8/u8/i16/u16/i32/u32/i64/u64/isize/usize, f32, f64, and bool. C spells isize as intptr_t, usize as size_t, and every mask lane as uint8_t, never _Bool. Other lane types remain valid inside Zynx but are rejected in extern ABI signatures.

Vectors may cross by value, by raw pointer, or inside recursively FFI-safe C-layout structs. Direct, byval, and sret lowering follows AAPCS64, SysV x86-64, or Win64 as selected by the target. Raw pointers to callable types use the same recursive gate for callback parameters and returns. Scalar bool and aggregate C variadics remain rejected.

Released under the MIT License.