Source
tests/purrc/linalg_cr_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
// Compile-run unit tests for the `linalg` module: one test per purr-callable4
// function. Each writes a tiny .purr that calls a single linalg routine on a5
// small fixed matrix/vector, compiles it with purrc, runs it under the cheatah6
// runtime, and asserts the exact stdout. Results are chosen to be integer-valued7
// (or exactly representable) so the io.print formatting is deterministic.8
// Complements the in-process unit tests (stdlib/tests/linalg_routines_test.cpp),9
// the per-module system test (StdlibE2E.Linalg), and the least-squares system10
// test (linalg_lsq_e2e_test.cpp).11
#include "e2e_harness.hpp"13
// ---- Matrix and vector products ----15
TEST(LinalgCompileRun, Dot) {16
e2e::expect_e2e("linalg_dot", R"PURR(import io17
import ndarray18
import linalg19
io.print(linalg.dot(ndarray.array([1.0, 2.0, 3.0]), ndarray.array([4.0, 5.0, 6.0])))20
)PURR", "32\n");21
}23
TEST(LinalgCompileRun, Vdot) {24
e2e::expect_e2e("linalg_vdot", R"PURR(import io25
import ndarray26
import linalg27
io.print(linalg.vdot(ndarray.array([1.0, 2.0, 3.0]), ndarray.array([4.0, 5.0, 6.0])))28
)PURR", "32\n");29
}31
TEST(LinalgCompileRun, Inner) {32
e2e::expect_e2e("linalg_inner", R"PURR(import io33
import ndarray34
import linalg35
io.print(linalg.inner(ndarray.array([1.0, 2.0, 3.0]), ndarray.array([4.0, 5.0, 6.0])))36
)PURR", "32\n");37
}39
TEST(LinalgCompileRun, Outer) {40
e2e::expect_e2e("linalg_outer", R"PURR(import io41
import ndarray42
import linalg43
io.print(ndarray.to_string(linalg.outer(ndarray.array([1.0, 2.0]), ndarray.array([3.0, 4.0]))))44
)PURR", "[[3, 4], [6, 8]]\n");45
}47
TEST(LinalgCompileRun, Matmul) {48
e2e::expect_e2e("linalg_matmul", R"PURR(import io49
import ndarray50
import linalg51
let a = ndarray.reshape(ndarray.array([1.0, 2.0, 3.0, 4.0]), [2, 2])52
let b = ndarray.reshape(ndarray.array([5.0, 6.0, 7.0, 8.0]), [2, 2])53
io.print(ndarray.to_string(linalg.matmul(a, b)))54
)PURR", "[[19, 22], [43, 50]]\n");55
}57
TEST(LinalgCompileRun, MatrixPower) {58
e2e::expect_e2e("linalg_matrix_power", R"PURR(import io59
import ndarray60
import linalg61
let a = ndarray.reshape(ndarray.array([2.0, 0.0, 0.0, 3.0]), [2, 2])62
io.print(ndarray.to_string(linalg.matrix_power(a, 3)))63
)PURR", "[[8, 0], [0, 27]]\n");64
}66
TEST(LinalgCompileRun, Kron) {67
e2e::expect_e2e("linalg_kron", R"PURR(import io68
import ndarray69
import linalg70
let a = ndarray.reshape(ndarray.array([1.0, 0.0, 0.0, 1.0]), [2, 2])71
let b = ndarray.reshape(ndarray.array([1.0, 2.0, 3.0, 4.0]), [2, 2])72
io.print(ndarray.to_string(linalg.kron(a, b)))73
)PURR", "[[1, 2, 0, 0], [3, 4, 0, 0], [0, 0, 1, 2], [0, 0, 3, 4]]\n");74
}76
// ---- Decompositions ----78
TEST(LinalgCompileRun, Cholesky) {79
e2e::expect_e2e("linalg_cholesky", R"PURR(import io80
import ndarray81
import linalg82
let a = ndarray.reshape(ndarray.array([4.0, 0.0, 0.0, 9.0]), [2, 2])83
io.print(ndarray.to_string(linalg.cholesky(a)))84
)PURR", "[[2, 0], [0, 3]]\n");85
}87
TEST(LinalgCompileRun, Qr) {88
// Diagonal input -> Householder gives diagonal R with the reflector's sign89
// convention (negated diagonal), which is exact and deterministic.90
e2e::expect_e2e("linalg_qr", R"PURR(import io91
import ndarray92
import linalg93
let a = ndarray.reshape(ndarray.array([6.0, 0.0, 0.0, 5.0]), [2, 2])94
let f = linalg.qr(a)95
io.print(ndarray.to_string(f.r))96
)PURR", "[[-6, 0], [0, -5]]\n");97
}99
TEST(LinalgCompileRun, Svd) {100
e2e::expect_e2e("linalg_svd", R"PURR(import io101
import ndarray102
import linalg103
let a = ndarray.reshape(ndarray.array([4.0, 0.0, 0.0, 9.0]), [2, 2])104
let f = linalg.svd(a)105
io.print(ndarray.to_string(f.s))106
)PURR", "[9, 4]\n");107
}109
TEST(LinalgCompileRun, Svdvals) {110
e2e::expect_e2e("linalg_svdvals", R"PURR(import io111
import ndarray112
import linalg113
let a = ndarray.reshape(ndarray.array([4.0, 0.0, 0.0, 9.0]), [2, 2])114
io.print(ndarray.to_string(linalg.svdvals(a)))115
)PURR", "[9, 4]\n");116
}118
// ---- Matrix eigenvalues ----120
TEST(LinalgCompileRun, Eig) {121
e2e::expect_e2e("linalg_eig", R"PURR(import io122
import ndarray123
import linalg124
let a = ndarray.reshape(ndarray.array([2.0, 0.0, 0.0, 3.0]), [2, 2])125
let e = linalg.eig(a)126
io.print(ndarray.to_string(e.values))127
)PURR", "[3+0j, 2+0j]\n"); // general eig -> complex spectrum (real parts here)128
}130
TEST(LinalgCompileRun, Eigvals) {131
e2e::expect_e2e("linalg_eigvals", R"PURR(import io132
import ndarray133
import linalg134
let a = ndarray.reshape(ndarray.array([2.0, 0.0, 0.0, 3.0]), [2, 2])135
io.print(ndarray.to_string(linalg.eigvals(a)))136
)PURR", "[3+0j, 2+0j]\n");137
}139
TEST(LinalgCompileRun, EigvalsComplex) {140
// A real rotation matrix [[0,-1],[1,0]] has eigenvalues ±i — printed Python-style.141
e2e::expect_e2e("linalg_eigvals_complex", R"PURR(import io142
import ndarray143
import linalg144
let a = ndarray.reshape(ndarray.array([0.0, -1.0, 1.0, 0.0]), [2, 2])145
io.print(ndarray.to_string(linalg.eigvals(a)))146
)PURR", "[0+1j, 0-1j]\n");147
}149
TEST(LinalgCompileRun, Eigh) {150
e2e::expect_e2e("linalg_eigh", R"PURR(import io151
import ndarray152
import linalg153
let a = ndarray.reshape(ndarray.array([2.0, 0.0, 0.0, 5.0]), [2, 2])154
let e = linalg.eigh(a)155
io.print(ndarray.to_string(e.values))156
)PURR", "[5, 2]\n");157
}159
TEST(LinalgCompileRun, Eigvalsh) {160
e2e::expect_e2e("linalg_eigvalsh", R"PURR(import io161
import ndarray162
import linalg163
let a = ndarray.reshape(ndarray.array([2.0, 0.0, 0.0, 5.0]), [2, 2])164
io.print(ndarray.to_string(linalg.eigvalsh(a)))165
)PURR", "[5, 2]\n");166
}168
// ---- Norms and other numbers ----170
TEST(LinalgCompileRun, Norm) {171
e2e::expect_e2e("linalg_norm", R"PURR(import io172
import ndarray173
import linalg174
io.print(linalg.norm(ndarray.array([3.0, 4.0])))175
)PURR", "5\n");176
}178
TEST(LinalgCompileRun, Cond) {179
e2e::expect_e2e("linalg_cond", R"PURR(import io180
import ndarray181
import linalg182
let a = ndarray.reshape(ndarray.array([2.0, 0.0, 0.0, 8.0]), [2, 2])183
io.print(linalg.cond(a))184
)PURR", "4\n");185
}187
TEST(LinalgCompileRun, Det) {188
e2e::expect_e2e("linalg_det", R"PURR(import io189
import ndarray190
import linalg191
let a = ndarray.reshape(ndarray.array([1.0, 2.0, 3.0, 4.0]), [2, 2])192
io.print(linalg.det(a))193
)PURR", "-2\n");194
}196
TEST(LinalgCompileRun, MatrixRank) {197
// Rank-1 matrix (second row = 2x first) -> numerical rank 1.198
e2e::expect_e2e("linalg_matrix_rank", R"PURR(import io199
import ndarray200
import linalg201
let a = ndarray.reshape(ndarray.array([1.0, 2.0, 2.0, 4.0]), [2, 2])202
io.print(linalg.matrix_rank(a))203
)PURR", "1\n");204
}206
TEST(LinalgCompileRun, Slogdet) {207
// det = 6 > 0, so the sign is exactly +1 (avoids the float logabsdet).208
e2e::expect_e2e("linalg_slogdet", R"PURR(import io209
import ndarray210
import linalg211
let a = ndarray.reshape(ndarray.array([2.0, 0.0, 0.0, 3.0]), [2, 2])212
let r = linalg.slogdet(a)213
io.print(r.sign)214
)PURR", "1\n");215
}217
TEST(LinalgCompileRun, Trace) {218
e2e::expect_e2e("linalg_trace", R"PURR(import io219
import ndarray220
import linalg221
let a = ndarray.reshape(ndarray.array([1.0, 2.0, 3.0, 4.0]), [2, 2])222
io.print(linalg.trace(a))223
)PURR", "5\n");224
}226
// ---- Solving equations and inverting matrices ----228
TEST(LinalgCompileRun, Solve) {229
// 4x+3y=10, 6x+3y=12 -> x=1, y=2.230
e2e::expect_e2e("linalg_solve", R"PURR(import io231
import ndarray232
import linalg233
let a = ndarray.reshape(ndarray.array([4.0, 3.0, 6.0, 3.0]), [2, 2])234
io.print(ndarray.to_string(linalg.solve(a, ndarray.array([10.0, 12.0]))))235
)PURR", "[1, 2]\n");236
}238
TEST(LinalgCompileRun, Lstsq) {239
// Identity system with a column-vector RHS -> x = b exactly.240
e2e::expect_e2e("linalg_lstsq", R"PURR(import io241
import ndarray242
import linalg243
let a = ndarray.reshape(ndarray.array([1.0, 0.0, 0.0, 1.0]), [2, 2])244
let b = ndarray.reshape(ndarray.array([5.0, 7.0]), [2, 1])245
io.print(ndarray.to_string(linalg.lstsq(a, b)))246
)PURR", "[[5], [7]]\n");247
}249
TEST(LinalgCompileRun, Inv) {250
e2e::expect_e2e("linalg_inv", R"PURR(import io251
import ndarray252
import linalg253
let a = ndarray.reshape(ndarray.array([2.0, 0.0, 0.0, 4.0]), [2, 2])254
io.print(ndarray.to_string(linalg.inv(a)))255
)PURR", "[[0.5, 0], [0, 0.25]]\n");256
}258
TEST(LinalgCompileRun, Pinv) {259
e2e::expect_e2e("linalg_pinv", R"PURR(import io260
import ndarray261
import linalg262
let a = ndarray.reshape(ndarray.array([2.0, 0.0, 0.0, 4.0]), [2, 2])263
io.print(ndarray.to_string(linalg.pinv(a)))264
)PURR", "[[0.5, 0], [0, 0.25]]\n");265
}267
// ---- complex products (complex inner-product spaces) ----269
TEST(LinalgCompileRun, ComplexDot) {270
e2e::expect_e2e("linalg_complex_dot", R"PURR(import io271
import ndarray272
import linalg273
let a = ndarray.complex(ndarray.array([1.0, 3.0]), ndarray.array([2.0, -1.0]))274
let b = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, 0.0]))275
io.print(linalg.dot(a, b))276
)PURR", "4-1j\n");277
}279
TEST(LinalgCompileRun, ComplexVdot) {280
e2e::expect_e2e("linalg_complex_vdot", R"PURR(import io281
import ndarray282
import linalg283
let a = ndarray.complex(ndarray.array([1.0, 3.0]), ndarray.array([2.0, -1.0]))284
let b = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, 0.0]))285
io.print(linalg.vdot(a, b))286
)PURR", "8+3j\n");287
}289
TEST(LinalgCompileRun, ComplexMatmul) {290
e2e::expect_e2e("linalg_complex_matmul", R"PURR(import io291
import ndarray292
import linalg293
let M = ndarray.reshape(ndarray.complex(ndarray.array([1.0, 0.0, 0.0, 1.0]), ndarray.array([1.0, 0.0, 0.0, 1.0])), [2, 2])294
let I = ndarray.reshape(ndarray.complex(ndarray.array([1.0, 0.0, 0.0, 1.0]), ndarray.array([0.0, 0.0, 0.0, 0.0])), [2, 2])295
io.print(ndarray.to_string(linalg.matmul(M, I)))296
)PURR", "[[1+1j, 0+0j], [0+0j, 1+1j]]\n");297
}299
TEST(LinalgCompileRun, ConjTranspose) {300
e2e::expect_e2e("linalg_conj_transpose", R"PURR(import io301
import ndarray302
import linalg303
let M = ndarray.reshape(ndarray.complex(ndarray.array([1.0, 2.0, 0.0, 3.0]), ndarray.array([1.0, 0.0, 0.0, -1.0])), [2, 2])304
io.print(ndarray.to_string(linalg.conj_transpose(M)))305
)PURR", "[[1-1j, 0+0j], [2+0j, 3+1j]]\n");306
}308
TEST(LinalgCompileRun, EighComplex) {309
// Hermitian [[2, 1+i],[1-i, 3]] -> real eigenvalues 4, 1.310
e2e::expect_e2e("linalg_eigh_complex", R"PURR(import io311
import ndarray312
import linalg313
let H = ndarray.reshape(ndarray.complex(ndarray.array([2.0, 1.0, 1.0, 3.0]), ndarray.array([0.0, 1.0, -1.0, 0.0])), [2, 2])314
io.print(ndarray.to_string(linalg.eigh(H).values))315
)PURR", "[4, 1]\n");316
}318
TEST(LinalgCompileRun, EigvalshComplex) {319
e2e::expect_e2e("linalg_eigvalsh_complex", R"PURR(import io320
import ndarray321
import linalg322
let H = ndarray.reshape(ndarray.complex(ndarray.array([2.0, 1.0, 1.0, 3.0]), ndarray.array([0.0, 1.0, -1.0, 0.0])), [2, 2])323
io.print(ndarray.to_string(linalg.eigvalsh(H)))324
)PURR", "[4, 1]\n");325
}