Skip to content

Matrix Types

Matrix<T, R, C> is a fixed-size scalar matrix value with R rows and C columns, stored row-major. It is not an array of arrays, and its structural C ABI spelling is struct { T elements[R][C]; }. For floating-point element types, * performs true matrix multiplication without any library call. Integer matrix arithmetic is checked by default: +, -, * (matmul and scaling) trap on overflow, and std.matrix.wrapping_mul is the explicit wrapping alternative.

The public matrix-product and transpose paths are deliberately closed:

  • a * b performs ordinary matrix multiplication; for integer elements every product and accumulation is checked;
  • std.matrix.wrapping_mul(a, b) performs integer matrix multiplication with wrapping products and accumulations;
  • std.matrix.transpose(m) transposes a matrix.

Raw backend spellings such as @llvm.matrix.multiply and @llvm.matrix.transpose are not source APIs and are rejected in ordinary code. The compiler-trusted SDK may implement the public functions through private, target-independent semantic intrinsic contracts. Their identity and semantics do not depend on LLVM or any other selected backend.

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

Type Rules

The element type T must be bool or a builtin numeric scalar type: floating-point (f16, bf16, f32, f64, f128) or integer (i8-i128, u8-u128, isize, usize, or custom iN/uN widths from 1 through 65535 bits). Bool-lane matrices are masks. References, pointers, and arrays are rejected.

Zynx
let a: Matrix<f64, 2, 3> = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let b: Matrix<f32, 2, 2> = [[1.5, 2.5], [3.5, 4.5]];
let c: Matrix<i32, 2, 2> = [[1, 2], [3, 4]];
let d: Matrix<u8, 2, 3> = [[1, 2, 3], [4, 5, 6]];
let mask: Matrix<bool, 2, 2> = [[true, false], [false, true]];

Both dimensions are checked usize shape expressions. Literals and const parameters may appear directly; compound expressions require braces, for example Matrix<T, R, {C + 1}>. Named const aliases, parentheses, integer casts, unary + - ~, and + - * / % << >> & | ^ are supported. Every intermediate operation is checked. Each dimension must be positive. A const generic of another integer type requires an explicit as usize cast, and its value is interpreted at the target's usize width. The total element count is limited to 65,536 and the object size to 16 MiB. Each matmul, matvec, or vecmat operation may expand to at most 1,000,000 scalar operations.

Matrix dimensions are always static. Runtime-sized tensors and reshape operations are separate concepts rather than implicit Matrix conversions.

Generic signatures may use Matrix<T, R, C> when T is a generic parameter with a Scalar bound (or a more specific Integer/Float bound):

Zynx
fn scale2<T: Float, R: usize, C: usize>(m: Matrix<T, R, C>) -> Matrix<T, R, C> {
	return m * 2.0;
}

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

Literals

Matrix values use nested array-literal syntax when the expected type is known: R rows of C elements each.

Zynx
let contextual: Matrix<f64, {2}, {2}> = [[1.0, 2.0], [3.0, 4.0]];
let inferred = Matrix([[1, 2], [3, 4]]);
let typed = Matrix<i64>([[1, 2], [3, 4]]);
let explicit = Matrix<i64, {2}, {2}>([[1, 2], [3, 4]]);
let rows: [[i32; 2]; 2] = [[1, 2], [3, 4]];
let from_rows = Matrix(rows);

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

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

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

There is no constructor widening rule or numeric conversion lattice. Use an explicit aggregate as Matrix<T, {R}, {C}> cast when a typed value's lane type must change. Explicitly written constructor dimensions, when present, must also match the argument shape exactly.

Matrix(...) is a call. Its expected result type never supplies or selects the call's element type or dimensions. Constructor inference considers explicit generic arguments first, then the nested fixed-array argument's element type and shape, and finally the default types of still-unconstrained literal elements. Only after the RHS call has a complete type is that result checked against the surrounding destination.

Consequently, this does not infer i64 from the declaration and is rejected as an ordinary result mismatch:

Zynx
// RHS independently infers Matrix<i32, {2}, {2}>.
let d: Matrix<i64, {2}, {2}> = Matrix([[1, 2], [3, 4]]);

Both intended spellings remain explicit and local:

Zynx
let called = Matrix<i64>([[1, 2], [3, 4]]);
let direct: Matrix<i64, {2}, {2}> = [[1, 2], [3, 4]];

The direct nested literal is contextual construction syntax, not call resolution, so it may use the expected Matrix element type and shape. Conflicting element types or dimensions are compile-time errors. A non-literal array argument must have the exact Matrix element type; use an explicit aggregate cast when lane conversion is required.

Matrix literals are supported in typed local declarations, function arguments, return expressions, and struct fields. Row and column counts must exactly match the type; a flat (non-nested) literal is rejected.

Inside a generic body, any shape equality that cannot be proved immediately creates an obligation. Obligations propagate through generic calls and are checked at each concrete instantiation, with the diagnostic identifying the instantiation site and both constraint origins. Shape expressions normalize constant subexpressions and the associative-commutative operators +, *, &, |, and ^, so forms such as N + 1 and 1 + N compare equal. The fill form can name symbolic dimensions directly:

Zynx
fn zero<T: Float, R: usize, C: usize>() -> Matrix<T, R, C> {
	let m: Matrix<T, R, C> = [[0.0; C]; R];
	return m;
}

Operations

GroupOperatorsOperandsSemantics
Elementwise arithmetic+ - / %matrices of identical Matrix<T, R, C> typeper-element arithmetic; integer operations are checked for overflow and invalid division/remainder
Hadamard product@hadamard(a, b)numeric matrices of identical type and shapeper-element multiplication; checked for integer elements
Bitwise/shift& | ^ << >>matching integer matricesper-element integer operation; standard signed >> is arithmetic with sign extension and standard unsigned >> is logical with zero fill; shift counts are checked
Matrix multiply*Matrix<T, M, K> * Matrix<T, K, N>true matrix multiplication; result is Matrix<T, M, N>; checked for integer elements
Matrix-vector*Matrix<T, M, K> * Vector<T, K>linear product returning Vector<T, M>
Vector-matrix*Vector<T, M> * Matrix<T, M, N>linear product returning Vector<T, N>
Scaling*matrix and scalar of the element type (either order)multiplies every element; checked for integer elements
Comparison== != < <= > >=matching numeric matricesreturns Matrix<bool, R, C> element-by-element
Mask logic! & | ^matching mask matriceseager element-wise boolean operations; mask ==/!= also return masks
Unary+ - ~numeric matrices for +/-, integer matrices for ~identity, lane-wise checked negation, or lane-wise bitwise complement

Matrix right shift follows scalar semantics independently for every element. For signed i8, i16, i32, i64, i128, and isize elements, >> equals 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 element traps; the same invalid constant element is rejected at compile time.

Zynx
let a: Matrix<f64, 2, 3> = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let b: Matrix<f64, 3, 2> = [[7.0, 8.0], [9.0, 10.0], [11.0, 12.0]];

let p: Matrix<f64, 2, 2> = a * b; // matrix product
let s = a * 2.0;                  // scaling, also `2.0 * a`
let t = a + a;                    // elementwise
let h = @hadamard(a, a);          // elementwise multiplication
let col: Vector<f64, 3> = [1.0, 2.0, 3.0];
let projected: Vector<f64, 2> = a * col;
let zero: Matrix<f64, 2, 3> = [[0.0; 3]; 2];
let mask = a > zero;
assert @all(mask);

Compound assignment is accepted only when the corresponding operator produces exactly the destination type. For example, Matrix<T, R, C> *= Matrix<T, C, C> is valid, while multiplication whose result has different dimensions is rejected.

The inner dimensions of a product must agree; mismatches are compile-time errors:

text
Matrix multiplication dimension mismatch: lhs columns must equal rhs rows

Matrix /, %, bitwise, and shift operators require another Matrix with the same shape and exact element type. +, -, /, %, and bitwise operators do not accept scalars; the only Matrix-scalar operator is scaling with *. @all and @any accept an exact Matrix<bool, R, C> mask and return scalar bool. @all is true exactly when every element is true; @any is true exactly when at least one element is true. Scalar bool has no identity overload and is used directly; numeric and other Matrices, nested arrays, and user-defined aggregates are rejected. There is no shape-polymorphic reduction protocol. @select requires an exact Matrix<bool, R, C> selector and eagerly selects element-by-element; scalar-bool selectors are rejected. Each branch independently may be an exact Matrix<T, R, C> or scalar exactly T, splatted to all elements. Both branches must independently have the same exact T; aggregate/aggregate, aggregate/scalar, scalar/aggregate, and scalar/scalar forms all produce Matrix<T, R, C>. Another aggregate kind or shape is rejected. There is no numeric promotion, implicit conversion, or contextual coercion from the sibling branch or expected result. Literals keep their normal default type; use an explicit cast or typed local when it does not equal T. This splat is specific to @select and does not extend Matrix mask operators.

Interpolation renders nested row-major brackets, for example [[1, 2], [3, 4]]. One scalar format spec is broadcast to every element, so try ^"{m:.2f}" formats every lane with .2f. Matrix expressions are evaluated once, masks render true/false, and a compact runtime formatter iterates elements in row-major order. Every otherwise legal Matrix shape is formattable; there is no per-hole or combined lane cap and no per-element generated code. Runtime output, allocation, and error cost follow ordinary formatting behavior.

Integer overflow semantics

Integer matrix arithmetic follows the checked-by-default policy. Every elementwise add/sub/multiply, every division, remainder, shift, scaling multiply, and — inside a matrix product — every per-cell product AND every accumulation step is validated before the operation completes. Division by zero, signed min / -1, and invalid shift counts trap as their scalar equivalents do:

text
Overflow error: mul overflow
Overflow error: add overflow

The checked integer matmul traps on overflow rather than silently wrapping. When wrapping (two's-complement) semantics are wanted explicitly, use std.matrix.wrapping_mul:

Zynx
import std.matrix;

let big: Matrix<i32, 1, 2> = [[100000, 0]];
let col: Matrix<i32, 2, 1> = [[100000], [0]];

let w = matrix.wrapping_mul(big, col); // w[0][0] == 1410065408 (wrapped)
// let t = big * col;                  // would trap: mul overflow

Indexing And Assignment

m[r] projects row r as a Vector<T, C> value; m[r][c] reads one element. Elements support assignment and compound assignment, including with runtime indices:

Zynx
var m: Matrix<f64, 2, 2> = [[1.0, 2.0], [3.0, 4.0]];
let row: Vector<f64, 2> = m[1];

m[0][1] = 9.5;
m[1][0] += 0.5;
m[0] = row;
m[1] += m[1];

For a row store, the Matrix base and row index are evaluated once, the row is bounds-checked, and only then is the right-hand side evaluated. Compound row assignment snapshots the old row before evaluating the RHS, validates the full result, and commits it with one final store. Thus m[r] = m[s] and m[r] += m[r] observe the state from before that store.

Matrix row and element projections are assignable places, but references cannot be formed to them: &m[r] and &m[r][c] are rejected. Taking a reference to the whole Matrix remains supported.

A direct literal out-of-bounds index is a compile-time error:

text
Matrix row index 2 out of bounds (rows 2)
Matrix column index 3 out of bounds (columns 3)

Dynamic indexes are checked at runtime; an out-of-bounds index traps:

text
Zynx: matrix index out of bounds at file.zx:4

std.matrix

The std.matrix module provides construction and transpose helpers (magic types have no methods):

Zynx
import std.matrix;

let t = matrix.transpose(m);            // Matrix<T, R, C> -> Matrix<T, C, R>
let f = matrix.fill<f64, 2, 3>(0.5);    // every element = 0.5
let i = matrix.identity<f64, 3>();      // 3x3 identity
let n = matrix.identity<i32, 3>();      // integer overloads exist too
let w = matrix.wrapping_mul(a, b);      // integer matmul that wraps

transpose supports Float, Integer, and bool-mask matrices. It produces Matrix<T, C, R> by permuting rows and columns. Its trusted SDK implementation uses a private target-independent transpose contract; the selected backend may lower that contract to a compile-time lane permutation, with no runtime loop.

Casts, Conversions, And ABI

Matrix literals are contextual construction syntax, not an implicit conversion for an array-valued variable. Explicit aggregate casts support [[S; C]; R] <-> Matrix<T, R, C> and lane-wise Matrix-to-Matrix conversion when every scalar S as T cast is legal and the logical shape is identical. Flat [T; R*C] arrays are not Matrix shapes; reshape and dynamic-slice conversions are unsupported. Constructors require an exact element type for a non-literal argument.

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

The target-independent compiler representation preserves the matrix element type and shape. A backend may use one flat vector value while computing it. Stored values use the canonical nested row-major layout [R x [C x T]]. In descriptive layout notation, the language guarantees:

  • sizeof(Matrix<T, R, C>) == R * C * sizeof(T);
  • the alignment is the alignment of T;
  • Matrix<bool, R, C> 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 flat vector values in SSA.

At a C ABI boundary the accepted element spellings are i8/u8/i16/u16/i32/u32/i64/u64/isize/usize, f32, f64, and bool. isize maps to intptr_t, usize to size_t, and mask elements map to uint8_t, never _Bool. Matrices may cross by value, raw pointer, or inside a recursively FFI-safe C-layout struct; AAPCS64, SysV x86-64, and Win64 use their target-specific direct, indirect, byval, and sret classifications.

Released under the MIT License.