Source
tests/purrc/ndarray_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 `ndarray` module: one test per function. Each4
// writes a tiny .purr that calls a single ndarray function, compiles it with purrc,5
// runs it under the cheatah runtime, and asserts the exact stdout. Complements the6
// in-process unit tests (stdlib/tests/ndarray_test.cpp) and the per-module7
// system-level test (StdlibE2E.Ndarray).8
//9
// Skipped (not callable from .purr):10
// - broadcast_to / broadcast_shapes: take std::vector<std::size_t>, but cheatah11
// `list<int>` lowers to std::vector<long long>, which doesn't convert.12
// - shape_of returns std::vector<long long>, which io.print can't stream directly;13
// it is exercised here by indexing the returned list (ShapeOf below).14
#include "e2e_harness.hpp"16
TEST(NdarrayCompileRun, Array) {17
e2e::expect_e2e("ndarray_array", R"PURR(import io18
import ndarray19
io.print(ndarray.to_string(ndarray.array([1.0, 2.0, 3.0])))20
)PURR", "[1, 2, 3]\n");21
}23
TEST(NdarrayCompileRun, Scalar) {24
e2e::expect_e2e("ndarray_scalar", R"PURR(import io25
import ndarray26
io.print(ndarray.to_string(ndarray.scalar(42.0)))27
)PURR", "42\n");28
}30
TEST(NdarrayCompileRun, Zeros) {31
e2e::expect_e2e("ndarray_zeros", R"PURR(import io32
import ndarray33
io.print(ndarray.to_string(ndarray.zeros([2, 3])))34
)PURR", "[[0, 0, 0], [0, 0, 0]]\n");35
}37
TEST(NdarrayCompileRun, Ones) {38
e2e::expect_e2e("ndarray_ones", R"PURR(import io39
import ndarray40
io.print(ndarray.to_string(ndarray.ones([4])))41
)PURR", "[1, 1, 1, 1]\n");42
}44
TEST(NdarrayCompileRun, Full) {45
e2e::expect_e2e("ndarray_full", R"PURR(import io46
import ndarray47
io.print(ndarray.to_string(ndarray.full([2, 2], 7.0)))48
)PURR", "[[7, 7], [7, 7]]\n");49
}51
TEST(NdarrayCompileRun, Arange) {52
e2e::expect_e2e("ndarray_arange", R"PURR(import io53
import ndarray54
io.print(ndarray.to_string(ndarray.arange(0.0, 5.0, 1.0)))55
)PURR", "[0, 1, 2, 3, 4]\n");56
}58
TEST(NdarrayCompileRun, Reshape) {59
e2e::expect_e2e("ndarray_reshape", R"PURR(import io60
import ndarray61
io.print(ndarray.to_string(ndarray.reshape(ndarray.array([1.0, 2.0, 3.0, 4.0]), [2, 2])))62
)PURR", "[[1, 2], [3, 4]]\n");63
}65
TEST(NdarrayCompileRun, Add) {66
e2e::expect_e2e("ndarray_add", R"PURR(import io67
import ndarray68
io.print(ndarray.to_string(ndarray.add(ndarray.array([1.0, 2.0, 3.0]), ndarray.scalar(10.0))))69
)PURR", "[11, 12, 13]\n");70
}72
TEST(NdarrayCompileRun, Sub) {73
e2e::expect_e2e("ndarray_sub", R"PURR(import io74
import ndarray75
io.print(ndarray.to_string(ndarray.sub(ndarray.array([5.0, 7.0, 9.0]), ndarray.scalar(1.0))))76
)PURR", "[4, 6, 8]\n");77
}79
TEST(NdarrayCompileRun, Mul) {80
e2e::expect_e2e("ndarray_mul", R"PURR(import io81
import ndarray82
io.print(ndarray.to_string(ndarray.mul(ndarray.array([2.0, 4.0, 6.0]), ndarray.scalar(0.5))))83
)PURR", "[1, 2, 3]\n");84
}86
TEST(NdarrayCompileRun, Divide) {87
e2e::expect_e2e("ndarray_divide", R"PURR(import io88
import ndarray89
io.print(ndarray.to_string(ndarray.divide(ndarray.array([2.0, 4.0, 6.0]), ndarray.scalar(2.0))))90
)PURR", "[1, 2, 3]\n");91
}93
TEST(NdarrayCompileRun, Sum) {94
e2e::expect_e2e("ndarray_sum", R"PURR(import io95
import ndarray96
io.print(ndarray.sum(ndarray.array([1.0, 2.0, 3.0, 4.0])))97
)PURR", "10\n");98
}100
TEST(NdarrayCompileRun, Mean) {101
e2e::expect_e2e("ndarray_mean", R"PURR(import io102
import ndarray103
io.print(ndarray.mean(ndarray.ones([4])))104
)PURR", "1\n");105
}107
TEST(NdarrayCompileRun, Get) {108
e2e::expect_e2e("ndarray_get", R"PURR(import io109
import ndarray110
io.print(ndarray.get(ndarray.array([1.0, 2.0, 3.0]), [2]))111
)PURR", "3\n");112
}114
TEST(NdarrayCompileRun, ShapeOf) {115
e2e::expect_e2e("ndarray_shape_of", R"PURR(import io116
import ndarray117
let s = ndarray.shape_of(ndarray.zeros([2, 3]))118
io.print(s[0], s[1])119
)PURR", "2 3\n");120
}122
TEST(NdarrayCompileRun, SizeOf) {123
e2e::expect_e2e("ndarray_size_of", R"PURR(import io124
import ndarray125
io.print(ndarray.size_of(ndarray.zeros([2, 3])))126
)PURR", "6\n");127
}129
TEST(NdarrayCompileRun, ToString) {130
e2e::expect_e2e("ndarray_to_string", R"PURR(import io131
import ndarray132
io.print(ndarray.to_string(ndarray.reshape(ndarray.array([1.0, 2.0, 3.0, 4.0]), [2, 2])))133
)PURR", "[[1, 2], [3, 4]]\n");134
}136
// io.print ABBREVIATES a large array with "..." (readable by default); a small one is full.137
TEST(NdarrayCompileRun, PrintAbbreviatesLargeArray) {138
e2e::expect_e2e("ndarray_print_trunc", R"PURR(import io139
import ndarray140
io.print(ndarray.arange(0.0, 2000.0, 1.0))141
)PURR",142
"[0, 1, 2, ..., 1997, 1998, 1999]\n");143
}145
// io.rprint shows the array RAW (exactly as stored) — no abbreviation.146
TEST(NdarrayCompileRun, RprintShowsArrayFull) {147
e2e::expect_e2e("ndarray_rprint_full", R"PURR(import io148
import ndarray149
io.rprint(ndarray.arange(0.0, 6.0, 1.0))150
)PURR",151
"[0, 1, 2, 3, 4, 5]\n");152
}154
TEST(NdarrayCompileRun, Complex) {155
e2e::expect_e2e("ndarray_complex", R"PURR(import io156
import ndarray157
let z = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, -3.0]))158
io.print(ndarray.to_string(z))159
)PURR", "[0+1j, 2-3j]\n");160
}162
TEST(NdarrayCompileRun, Conj) {163
e2e::expect_e2e("ndarray_conj", R"PURR(import io164
import ndarray165
let z = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, -3.0]))166
io.print(ndarray.to_string(ndarray.conj(z)))167
)PURR", "[0-1j, 2+3j]\n");168
}170
TEST(NdarrayCompileRun, Real) {171
e2e::expect_e2e("ndarray_real", R"PURR(import io172
import ndarray173
let z = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, -3.0]))174
io.print(ndarray.to_string(ndarray.real(z)))175
)PURR", "[0, 2]\n");176
}178
TEST(NdarrayCompileRun, Imag) {179
e2e::expect_e2e("ndarray_imag", R"PURR(import io180
import ndarray181
let z = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, -3.0]))182
io.print(ndarray.to_string(ndarray.imag(z)))183
)PURR", "[1, -3]\n");184
}186
TEST(NdarrayCompileRun, Sqrt) {187
e2e::expect_e2e("ndarray_sqrt", R"PURR(import io188
import ndarray189
io.print(ndarray.to_string(ndarray.sqrt(ndarray.array([1.0, 4.0, 9.0, 16.0]))))190
)PURR", "[1, 2, 3, 4]\n");191
}193
TEST(NdarrayCompileRun, Exp) {194
e2e::expect_e2e("ndarray_exp", R"PURR(import io195
import ndarray196
io.print(ndarray.to_string(ndarray.exp(ndarray.array([0.0]))))197
)PURR", "[1]\n");198
}200
TEST(NdarrayCompileRun, Sin) {201
e2e::expect_e2e("ndarray_sin", R"PURR(import io202
import ndarray203
io.print(ndarray.to_string(ndarray.sin(ndarray.array([0.0]))))204
)PURR", "[0]\n");205
}207
TEST(NdarrayCompileRun, NestedArray) {208
// Nested list literals build N-D arrays directly: 2-D and 3-D from source.209
e2e::expect_e2e("ndarray_nested", R"PURR(import io210
import ndarray211
let m = ndarray.array([[1.0, 2.0], [3.0, 4.0]])212
io.print(ndarray.shape_of(m)[0], ndarray.shape_of(m)[1])213
io.print(ndarray.to_string(m))214
let t = ndarray.array([[[1.0], [2.0]], [[3.0], [4.0]]])215
io.print(ndarray.to_string(t))216
)PURR", "2 2\n[[1, 2], [3, 4]]\n[[[1], [2]], [[3], [4]]]\n");217
}219
TEST(NdarrayCompileRun, Astype) {220
// `arr.astype(<width>)` converts the element type — numpy's a.astype(dtype). The narrow221
// element makes a smaller array (sizeof(i16) == 2), and i8/u8 elements print as NUMBERS.222
e2e::expect_e2e("ndarray_astype", R"PURR(import io223
import ndarray224
let a = ndarray.array([1, 2, 3]).astype(i16)225
io.print(ndarray.to_string(a))226
io.print(sizeof(i16))227
let b = ndarray.array([65, 200, 9]).astype(u8)228
io.print(ndarray.to_string(b))229
let f = ndarray.array([1, 2, 3]).astype(f32)230
io.print(ndarray.to_string(f))231
)PURR", "[1, 2, 3]\n2\n[65, 200, 9]\n[1, 2, 3]\n");232
}234
TEST(NdarrayCompileRun, AstypeNarrows) {235
// Narrowing truncates/wraps at the target width, like a numpy fixed dtype: 300 -> 44 in u8.236
e2e::expect_e2e("ndarray_astype_narrow", R"PURR(import io237
import ndarray238
io.print(ndarray.to_string(ndarray.array([300, 256, 255]).astype(u8)))239
)PURR", "[44, 0, 255]\n");240
}242
TEST(NdarrayCompileRun, AstypeConversionsPrintSensibleNumbers) {243
// Narrowing/widening from cheatah source must print SENSIBLE NUMERIC values (never characters),244
// with C / numpy fixed-dtype semantics: signed narrowing wraps two's-complement, unsigned245
// narrowing is modulo 2^bits, signed->unsigned same width reinterprets the bits, float->int246
// truncates toward zero, a widen round-trip recovers the value, and shape is preserved in 2-D.247
e2e::expect_e2e("ndarray_astype_conversions", R"PURR(import io248
import ndarray249
fn main() {250
io.print(ndarray.to_string(ndarray.array([127, 128, 255, 256, -1, -129]).astype(i8)))251
io.print(ndarray.to_string(ndarray.array([0, 255, 256, 300, -1]).astype(u8)))252
io.print(ndarray.to_string(ndarray.array([-1, -2, 5]).astype(u32)))253
io.print(ndarray.to_string(ndarray.array([3.9, -3.9, 2.99, 255.7]).astype(i32)))254
io.print(ndarray.to_string(ndarray.array([-128, 0, 127]).astype(i8).astype(i64)))255
io.print(ndarray.to_string(ndarray.array([[1, 300], [256, -1]]).astype(u8)))256
io.print(ndarray.to_string(ndarray.array([1, 2, 3]).astype(f64)))257
}258
main()259
)PURR",260
"[127, -128, -1, 0, -1, 127]\n"261
"[0, 255, 0, 44, 255]\n"262
"[4294967295, 4294967294, 5]\n"263
"[3, -3, 2, 255]\n"264
"[-128, 0, 127]\n"265
"[[1, 44], [0, 255]]\n"266
"[1, 2, 3]\n");267
}269
// Both spellings of a width name reach astype identically, and a declared narrow type prints the270
// same sensible numbers as the explicit .astype form.271
TEST(NdarrayCompileRun, AstypeSpellingsAndDeclaredAgree) {272
e2e::expect_e2e("ndarray_astype_spellings", R"PURR(import io273
import ndarray274
fn main() {275
io.print(ndarray.to_string(ndarray.array([1, 2, 300]).astype(int16)))276
io.print(ndarray.to_string(ndarray.array([1, 2, 300]).astype(i16)))277
let a: ndarray<i16> = ndarray.array([1, 2, 300])278
io.print(ndarray.to_string(a))279
}280
main()281
)PURR", "[1, 2, 300]\n[1, 2, 300]\n[1, 2, 300]\n");282
}284
TEST(NdarrayCompileRun, NarrowElementDeclaredTypeDrives) {285
// A declared `ndarray<i8>` drives construction: the initializer is converted for you, so you286
// do not have to spell `.astype(i8)`. The 2-D shape and numeric i8 printing are preserved.287
e2e::expect_e2e("ndarray_narrow_decl", R"PURR(import io288
import ndarray289
fn main() {290
let a: ndarray<i8> = ndarray.array([100, 101, 102])291
io.print(ndarray.to_string(a))292
let m: ndarray<u16> = ndarray.array([[1, 2], [3, 4]])293
io.print(ndarray.to_string(m))294
}295
main()296
)PURR", "[100, 101, 102]\n[[1, 2], [3, 4]]\n");297
}