NiaLang is a small experimental compiled language for numeric, linear
algebra, and quantum-programming experiments. It treats vectors and matrices as
first-class values, has a compact Rust-like syntax, lowers native programs
through LLVM IR and clang, and can emit QIR for the bundled quantum runner.
The language is still experimental, but the core idea is already visible:
let dot = u @ v; // dot product
let sum = u + v; // element-wise vector addition
let had = u * v; // Hadamard product
let scaled = 3 * u; // scalar-vector multiplication
let ab = A @ B; // matrix product
let av = A @ v; // matrix-vector product
let va = v @ A; // vector-matrix product
let d = A.det(); // determinant
The goal is a language that feels natural for dense numeric and quantum code while staying simple enough to understand and hack on.
Requirements:
- a recent Rust toolchain;
clangfor native executables, assembly, and shared libraries;- the optional
qir-runnerfeature for quantum programs.
Build and run an example:
cargo run -- examples/sample_linalg_commented.niaBuild a shared library from exported extern fn symbols:
cargo run -- examples/sample_extern_lib.nia --lib -o build/libnia_sample.dylibEmit native assembly for inspection:
cargo run -- examples/sample_floats.nia --emit-asm build/sample_floats.sEmit textual LLVM IR:
cargo run -- examples/sample_all.nia --emit-ll build/sample_all.llCompile and run a QIR quantum sample:
cargo run -r --features qir-runner -- examples/quantum/qubit_create.nia -qRun the fixed N = 15 Shor demonstration:
cargo run -r --features qir-runner -- examples/quantum/qubit_shore.nia -qRun the compiler test suite:
cargo testNia programs usually look like this:
fn main() i32 {
let x = 40 + 2;
println(x);
0
}
Modules follow Rust-style item paths:
mod math {
fn add(a: i32, b: i32) i32 {
a + b
}
}
fn main() i32 {
math::add(40, 2)
}
When compiling from a file, mod math; loads math.nia or math/mod.nia.
Nested modules can use self::, super::, and crate:: paths. Privacy is not
implemented yet; pub is accepted as a no-op.
CLI modes:
| Command | Behavior |
|---|---|
nialang file.nia |
compile with clang and run |
nialang file.nia -o out.ll |
run and also keep generated LLVM IR |
nialang file.nia --emit-ll [out.ll] |
write LLVM IR without running |
nialang file.nia --emit-asm [out.s] |
write native assembly |
nialang file.nia --lib -o library |
build a shared library from extern fn exports |
nialang file.nia -q [-o out.ll] |
lower to QIR and run through qir-runner |
All integer types support arithmetic, remainder, bitwise operations, and compound assignment:
| Operator | Meaning |
|---|---|
+, -, *, / |
integer arithmetic |
a % b |
signed or unsigned remainder |
a & b |
bitwise AND |
a | b |
bitwise OR |
a ^ b |
bitwise XOR |
~a |
bitwise complement |
a << n |
left shift |
a >> n |
arithmetic shift for signed integers, logical shift for unsigned integers |
The corresponding compound assignments are available: +=, -=, *=, /=,
%=, &=, |=, ^=, <<=, and >>=.
Booleans support logical negation with !value.
Numeric literals may use _ between digits for readability:
let population = 1_000_000;
let ratio = 3.141_592;
let scale = 1.0e1_0;
let flags: u8 = 12;
let masked = flags & 10;
let rotated_part = flags << 2;
let odd = (flags & 1) == 1;
Named vectors are fixed-size vectors whose coordinates have labels. The labels make output easier to read and help keep examples close to math notation.
vector Vec2 i32 [X, Y]
fn main() i32 {
let u = Vec2 [X: 1, Y: 2];
let v = Vec2 [X: 3, Y: 4];
println(u + v); // (i32 {"X": 4, "Y": 6})
println(v - u); // (i32 {"X": 2, "Y": 2})
println(u * v); // (i32 {"X": 3, "Y": 8})
println(u @ v); // 11
println(3 * u); // (i32 {"X": 3, "Y": 6})
0
}
Vector operators:
| Operator | Meaning | Result |
|---|---|---|
u + v |
element-wise addition | vector |
u - v |
element-wise subtraction | vector |
u * v |
element-wise multiplication | vector |
u @ v |
dot product | scalar |
k * u, u * k |
scalar multiplication | vector |
-u |
element-wise negation | vector |
For quick numeric code, vectors can be written directly without declaring a named vector type:
fn main() i32 {
let a: i32<4> = <1, 2, 3, 4>;
let b: i32<4> = <10, 20, 30, 40>;
println(a + b); // [11, 22, 33, 44]
println(a * b); // [10, 40, 90, 160]
println(a @ b); // 300
0
}
The explicit anonymous-vector type spelling is T<N>: element type T,
length N. The same arithmetic works for integer and float vectors.
Use T<> for a reference-counted heap anonymous vector:
let v: f64<> = <1.0, 2.0, 3.0>;
println(vector_len(v));
println(len(v));
println(vector_refcount(v));
let shared: f64<> = vector_clone(v);
vector_set(shared, 1, 9.0);
println(vector_get(v, 1));
vector_drop(shared);
vector_drop(v);
List[T] is a growable heap-backed list for values of type T.
Constructors take the element type in brackets:
let bytes = list_new[u8]();
let zs: List[Complex] = list_with_capacity[Complex](2);
bytes.push(10);
bytes.push(20);
println(bytes.len());
println(bytes.capacity());
println(bytes.get(1));
The first list surface is intentionally small: len, capacity, push, and
get. Index syntax and explicit list cleanup are not implemented yet.
Complex is a built-in struct-shaped type with f64 fields:
let z = complex(1.0, 2.0);
let w = Complex { re: 3.0, im: 4.0 };
let sum = complex_add(z, w);
let product = complex_mul(sum, cis(PI));
let scaled = complex_scale(product, 0.5);
let ratio = complex_div(scaled, complex(1.0, -1.0));
println(ratio);
println(sin(PI) + cos(0.0));
Available helpers: complex, complex_add, complex_sub, complex_mul,
complex_scale, complex_div, sin, cos, PI, and cis.
Nia exposes SHA-256 and Merkle tree helpers as compiler builtins, so no
import or source prelude is required:
let data: [[u8; 3]; 2] = [[1, 2, 3], [4, 5, 6]];
let root = merkle_root_from_data(data);
let left = merkle_leaf_hash(data[0]);
let right = merkle_leaf_hash(data[1]);
let proof: [[u8; 32]; 1] = [right];
println(digest_eq(root, merkle_node_hash(left, right)));
println(merkle_verify(root, left, 0, proof));
The digest type is [u8; 32]. sha256 and merkle_leaf_hash accept fixed
byte arrays [u8; N]; merkle_root accepts [[u8; 32]; N];
merkle_root_from_data accepts fixed-size leaves [[u8; M]; N]; and
merkle_verify accepts (root, leaf, index, proof) where proof is
[[u8; 32]; D].
Merkle hashing uses domain separation:
- leaf hash:
SHA256(0x00 || data) - internal node hash:
SHA256(0x01 || left || right)
For odd leaf counts, the last node is duplicated at that level.
Matrices are built with matrix([...]):
let A = matrix([
[1, 2, 3],
[4, 5, 6],
]);
let B = matrix([
[7, 8, 9],
[10, 11, 12],
]);
A matrix is a built-in heap-backed value. Internally the compiler tracks the
element type and dimensions when it can. User code annotates the type as T[]
where T is the element type (e.g. i32[], f64[]).
Matrices support element-wise addition, subtraction, and multiplication:
fn main() i32 {
let a = matrix([
[1, 2],
[3, 4],
]);
let b = matrix([
[10, 20],
[30, 40],
]);
println(a + b); // [[11, 22], [33, 44]]
println(b - a); // [[9, 18], [27, 36]]
println(a * b); // [[10, 40], [90, 160]]
println(a * 3); // [[3, 6], [9, 12]]
matrix_drop(a);
matrix_drop(b);
0
}
Use @ for the linear algebra matrix product:
fn main() i32 {
let a = matrix([
[1, 2, 3],
[4, 5, 6],
]);
let b = matrix([
[7, 8],
[9, 10],
[11, 12],
]);
let c = a @ b;
println(c); // [[58, 64], [139, 154]]
matrix_drop(a);
matrix_drop(b);
matrix_drop(c);
0
}
The dimensions follow the usual rule:
(m x n) @ (n x p) -> (m x p)
@ also works between matrices and vectors:
vector Vec3 i32 [X, Y, Z]
vector Vec2 i32 [R, S]
fn main() i32 {
let a = matrix([
[1, 2, 3],
[4, 5, 6],
]);
let x = Vec3 [X: 10, Y: 20, Z: 30];
let y = Vec2 [R: 7, S: 8];
let ax: Vec2 = a @ x;
let ya: Vec3 = y @ a;
println(ax); // (i32 {"R": 140, "S": 320})
println(ya); // (i32 {"X": 39, "Y": 54, "Z": 69})
matrix_drop(a);
0
}
Rules:
Matrix(m x n) @ Vector(n) -> Vector(m)
Vector(m) @ Matrix(m x n) -> Vector(n)
Anonymous vectors use the same operator:
let left = matrix([
[1, 2, 3],
[4, 5, 6],
]) @ <10, 20, 30>;
let right = <7, 8> @ matrix([
[1, 2, 3],
[4, 5, 6],
]);
println(left); // [140, 320]
println(right); // [39, 54, 69]
The outer builtin builds a matrix from two vectors:
let u = <1, 2, 3>;
let v = <10, 20>;
let m = outer(u, v);
println(m); // [[10, 20], [20, 40], [30, 60]]
matrix_drop(m);
The determinant is exposed as a Matrix method:
fn main() i32 {
let m = matrix([
[1, 2, 0, 1, 3],
[2, 5, 1, 0, 4],
[0, 1, 3, 2, 1],
[1, 0, 2, 4, 2],
[3, 1, 0, 2, 5],
]);
println(m.det());
matrix_drop(m);
0
}
Only square matrices have determinants.
The project includes a larger matrix-vector example with roughly a thousand elements and non-uniform generated values:
cargo run -- examples/sample_matrix_vector_large.niaIt is useful as a smoke test for generated loops and larger dense values.
Matrix values are reference-counted heap handles in the runtime. For now,
matrix lifetime management is explicit:
let m = matrix([
[1, 2],
[3, 4],
]);
println(m.det());
matrix_drop(m);
This is intentionally simple while the language is young. Long term, this is one of the areas where the compiler can grow more ownership and lifetime help.
NiaLang is not only a matrix calculator. It has a small general-purpose core around the linear algebra features.
Primitive types:
let i: i32 = 42;
let f: f64 = 3.14;
let ok: bool = true;
let msg: string = "hello";
Arrays:
let xs = [1, 2, 3, 4];
println(xs[0]);
Pointers and heap allocation:
let p: &i32 = alloc(123);
println(*p);
*p = 456;
let moved: &i32 = realloc(p, 789);
dealloc(moved);
fn main() i32 {
let n = 5;
if n > 0 {
println("positive");
}
let acc = 0;
let i = 0;
while i < 5 {
acc = acc + i;
i = i + 1;
}
for value in 0..3 {
println(value);
}
loop {
println("once");
break;
}
println(acc);
0
}
if currently has no else branch. break is supported by loop; breaking
from while or for is not implemented yet.
gpu { ... } is currently a normal scoped block reserved for future specialized
behavior: bindings declared inside do not escape, while assignments to outer
variables still work.
fn main() i32 {
let x = 1;
let y = 0;
gpu {
let local = 41;
y = x + local;
}
y
}
NiaLang has a QIR backend for small static quantum programs. Quantum code is
written inside quant { ... } blocks or quant fn functions. The current
surface includes qubit registers, single-, controlled-, and three-qubit gates,
constant-angle rotations, Z-basis measurement, classical result reads, and QIR
output recording.
quant fn bell(control: qubit, target: qubit) {
H(control);
CNOT(control, target);
}
quant fn echo_h(q: qubit) {
for i in 0..2 {
H(q);
}
}
quant fn flip(q: qubit) {
X(q);
}
quant fn phase_like(y: qubit, z: qubit, s: qubit, t: qubit) {
Y(y);
Z(z);
S(s);
T(t);
}
quant fn controlled_phase(control: qubit, target: qubit) {
CZ(control, target);
}
quant fn swap_pair(left: qubit, right: qubit) {
SWAP(left, right);
}
quant fn rotate_like(rx: qubit, ry: qubit, rz: qubit, r1: qubit) {
Rx(PI / 2.0, rx);
Ry(PI / 4.0, ry);
Rz(PI / 8.0, rz);
R1(PI, r1);
}
quant fn identity_and_adjoint(i: qubit, s: qubit, t: qubit) {
I(i);
Sdg(s);
Tdg(t);
}
quant fn controlled_more(control: qubit, h: qubit, y: qubit, s: qubit, t: qubit) {
CH(control, h);
CY(control, y);
CS(control, s);
CSdg(control, s);
CT(control, t);
CTdg(control, t);
}
quant fn three_qubit_like(control_a: qubit, control_b: qubit, target: qubit) {
CCNOT(control_a, control_b, target);
CCZ(control_a, control_b, target);
CSWAP(control_a, control_b, target);
}
quant fn controlled_rotations(control: qubit, x: qubit, y: qubit, z: qubit, p: qubit) {
CRx(PI / 2.0, control, x);
CRy(PI / 4.0, control, y);
CRz(PI / 8.0, control, z);
CR1(PI, control, p);
}
fn main() i32 {
quant {
let a = qubit();
let b = qubit();
let x = qubit();
let y = qubit();
let z = qubit();
let s = qubit();
let t = qubit();
bell(a, b);
echo_h(a);
controlled_phase(a, b);
flip(x);
phase_like(y, z, s, t);
rotate_like(y, z, s, t);
identity_and_adjoint(x, s, t);
controlled_more(a, y, z, s, t);
three_qubit_like(a, b, x);
controlled_rotations(a, x, y, z, t);
swap_pair(s, t);
let ar = q_measure(a);
let br = q_measure(b);
let xr = q_measure(x);
let yr = q_measure(y);
let zr = q_measure(z);
let sr = q_measure(s);
let tr = q_measure(t);
q_record(ar);
q_record(br);
q_record(xr);
q_record(yr);
q_record(zr);
q_record(sr);
q_record(tr);
}
0
}
The quantum surface is intentionally small:
| Syntax | Meaning |
|---|---|
quant { ... } |
quantum scope; quantum resources cannot escape it |
quant fn Name(...) { ... } |
quantum function; callable only from quant scopes |
for i in A..B { ... } |
static quantum loop; QIR lowering unrolls compile-time integer ranges |
qubit() |
create a qubit resource inside quant |
I(q) |
identity gate; leaves a qubit unchanged |
H(q) |
apply the Hadamard gate to a qubit |
X(q) |
apply the Pauli-X gate; flips ` |
Y(q) |
apply the Pauli-Y gate; bit flip with phase |
Z(q) |
apply the Pauli-Z gate; phase flip on ` |
S(q) |
apply the phase gate, a pi/2 Z-axis phase rotation |
Sdg(q) |
apply the inverse of S(q) |
T(q) |
apply the T gate, a pi/4 Z-axis phase rotation |
Tdg(q) |
apply the inverse of T(q) |
CNOT(c, t) |
controlled-X: flips target t when control c is ` |
CZ(c, t) |
controlled-Z: applies a phase flip to t when control c is ` |
SWAP(a, b) |
swap the quantum states of two qubits |
CH(c, t) |
controlled-H: applies H(t) when control c is ` |
CY(c, t) |
controlled-Y: applies Y(t) when control c is ` |
CS(c, t) |
controlled-S: applies S(t) when control c is ` |
CSdg(c, t) |
controlled inverse-S: applies Sdg(t) when control c is ` |
CT(c, t) |
controlled-T: applies T(t) when control c is ` |
CTdg(c, t) |
controlled inverse-T: applies Tdg(t) when control c is ` |
CCNOT(a, b, t) |
Toffoli gate: applies X(t) when both controls are ` |
CCZ(a, b, t) |
controlled-controlled-Z phase flip |
CSWAP(c, a, b) |
Fredkin gate: swaps a and b when control c is ` |
Rx(theta, q) |
rotate a qubit around the X axis by a constant f64 angle |
Ry(theta, q) |
rotate a qubit around the Y axis by a constant f64 angle |
Rz(theta, q) |
rotate a qubit around the Z axis by a constant f64 angle |
R1(theta, q) |
apply a constant phase rotation |
CRx(theta, c, t) |
controlled X-axis rotation by a constant f64 angle |
CRy(theta, c, t) |
controlled Y-axis rotation by a constant f64 angle |
CRz(theta, c, t) |
controlled Z-axis rotation by a constant f64 angle |
CR1(theta, c, t) |
controlled phase rotation by a constant f64 angle |
q_measure(q) |
measure a qubit in the Z basis and return result |
q_read(r) |
read a result as a classical bool in QIR |
q_record(x) |
record a result or bool as QIR output |
qubit and result are quantum-only types. They cannot be returned from a
quant expression or printed with println; use q_record(r) to expose raw
measurement output to the QIR runner, or q_read(r) to turn a measurement
result into a classical bool before recording it. quant fn bodies are
checked as quantum scopes, so they can create qubits directly. Calls to
quant fn are rejected outside quant { ... }.
The current QIR lowering supports void quant fn calls with qubit,
[qubit; N], and result parameters. Returning values from quantum functions
is reserved for future work. Rotation angles must be compile-time expressions
such as PI, PI / 2.0, or 0.125 + 0.125. Quantum register sizes and
quantum for ranges are static. Measurement results can be converted to
classical bool values with q_read and used by ordinary control flow.
Run the current sample:
cargo run -r --features qir-runner -- examples/quantum/qubit_create.nia -qThe runner output includes QIR metadata and recorded measurement results:
START
METADATA entry_point
METADATA qir_profiles adaptive_profile
METADATA required_num_qubits 0
METADATA required_num_results 0
OUTPUT RESULT 1
OUTPUT RESULT 0
OUTPUT RESULT 1
OUTPUT RESULT 1
OUTPUT RESULT 1
OUTPUT RESULT 0
OUTPUT RESULT 0
END 0
Because H(q) creates a superposition and CNOT(c, t) entangles the two
qubits, the recorded results can vary between runs. The same sample also lowers
CZ(c, t), SWAP(a, b), adjoint phase gates, controlled gates, three-qubit
gates, constant-angle rotations, and controlled rotations. You can also write
the generated QIR IR to a file:
cargo run -r --features qir-runner -- examples/quantum/qubit_create.nia -q -o build/qubit_create.llMore complete examples include QFT and inverse QFT, Deutsch-Jozsa, a measured
random bit, and a specialized Shor factorization circuit for N = 15. The Shor
sample can produce an inconclusive phase and request another run, as expected
for a probabilistic order-finding procedure.
struct Point {
x: i32,
y: i32,
}
enum Shape {
Dot,
Circle(i32),
Rect(i32, i32),
}
fn area(shape: Shape) i32 {
match shape {
Shape::Dot => 0,
Shape::Circle(r) => r * r,
Shape::Rect(w, h) => w * h,
}
}
NiaLang has Rust-style impl blocks as syntax sugar over normal functions:
struct Counter {
value: i32,
}
impl Counter {
fn new(value: i32) Counter {
Counter { value: value }
}
fn inc(self) Counter {
Counter { value: self.value + 1 }
}
fn get(&self) i32 {
self.value
}
}
fn main() i32 {
let c = Counter::new(10).inc();
println(c.get());
0
}
Both self and &self are supported in method syntax. Mutable self is not part
of the language yet.
Good places to start:
| File | What it shows |
|---|---|
examples/sample_linalg_commented.nia |
guided linear algebra tour |
examples/sample_vector.nia |
named vector basics |
examples/sample_vector_arith.nia |
vector arithmetic |
examples/sample_anon_vector.nia |
anonymous vectors |
examples/sample_matrix_arith.nia |
matrix arithmetic and multiplication |
examples/sample_matrix_vector.nia |
matrix-vector and vector-matrix products |
examples/sample_matrix_vector_large.nia |
larger dense matrix-vector smoke test |
examples/sample_matrix_det.nia |
determinant as m.det() |
examples/sample_complex.nia |
complex numbers, trig, and cis |
examples/sample_dft4.nia |
discrete Fourier transform for a 4-value signal |
examples/tests/ok_bitwise.nia |
remainder, bitwise operators, shifts, and compound assignment |
examples/sample_list.nia |
dynamic List[T] constructors and methods |
examples/sample_dft_list.nia |
list-backed discrete Fourier transform |
examples/sample_matrix_rc.nia |
explicit matrix lifetime management |
examples/sample_impl_methods.nia |
impl, self, and &self |
examples/sample_closures.nia |
non-capturing closure/function-value smoke test |
examples/sample_extern_lib.nia |
C ABI exports and shared-library mode |
examples/quantum/qubit_create.nia |
QIR gates, rotations, measurement, and result recording |
examples/quantum/qubit_read.nia |
read a QIR measurement result as bool with q_read |
examples/quantum/measure_qubits_as_byte.nia |
sample a biased 8-qubit cat state and collect byte statistics |
examples/quantum/random_bit.nia |
measurement-driven classical control |
examples/quantum/deutsch_jozsa_1bit.nia |
one-bit Deutsch-Jozsa circuits |
examples/quantum/qft4.nia |
4-qubit quantum Fourier transform over a qubit register |
examples/quantum/iqft4.nia |
inverse 4-qubit QFT, composed with QFT as a round-trip check |
examples/quantum/qubit_shore.nia |
specialized Shor factorization demo for N = 15, a = 2 |
examples/sample_all.nia |
broad language feature sample |
NiaLang is an experimental compiler and language playground.
Currently available:
- signed and unsigned integer types, floating-point types, strings, and booleans
- scalar arithmetic,
%, bitwise operators, shifts, logical!, and compound assignment - functions,
if,while, static rangefor, andloopwithbreak - fixed arrays with indexing and mutation
- structs, tuple structs, enums, pattern matching, pointers, and heap allocation
extern fnC ABI exports and shared-library builds- named and anonymous vectors
- dynamic
List[T]values withlen,capacity,push, andget - complex numbers,
sin,cos,PI, andcis - dense reference-counted matrices
- vector arithmetic, dot products, scalar multiplication, and outer products
- matrix arithmetic, matrix multiplication, and array conversions
- matrix-vector and vector-matrix multiplication
- determinant as a
Matrixmethod - Rust-style
implmethod syntax - QIR quantum blocks/functions with qubit arrays, controlled and three-qubit gates, rotations, measurement, result reads, and recording
Still intentionally small or unfinished:
- no
elsebranch yet breakworks only withloop, notwhileorfor- no sparse matrices
- no eigenvalues, QR, SVD, or advanced decomposition APIs
- no list index syntax or explicit list cleanup yet
- quantum register sizes, quantum loops, and rotation angles are static
- quantum functions are currently void and do not have general
controlled/adjointgeneration - no generic register-size parameters or universal
shor(N)implementation - explicit matrix lifetime management
- limited diagnostics compared with production languages
- experimental syntax and type inference
The sweet spot today is compact compiler experiments, dense numeric programs, and small quantum circuits that should read close to the underlying math.