cheatah
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. Each
4// 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 the
6// in-process unit tests (stdlib/tests/ndarray_test.cpp) and the per-module
7// system-level test (StdlibE2E.Ndarray).
8//
9// Skipped (not callable from .purr):
10// - broadcast_to / broadcast_shapes: take std::vector<std::size_t>, but cheatah
11// `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"
16TEST(NdarrayCompileRun, Array) {
17 e2e::expect_e2e("ndarray_array", R"PURR(import io
18import ndarray
19io.print(ndarray.to_string(ndarray.array([1.0, 2.0, 3.0])))
20)PURR", "[1, 2, 3]\n");
23TEST(NdarrayCompileRun, Scalar) {
24 e2e::expect_e2e("ndarray_scalar", R"PURR(import io
25import ndarray
26io.print(ndarray.to_string(ndarray.scalar(42.0)))
27)PURR", "42\n");
30TEST(NdarrayCompileRun, Zeros) {
31 e2e::expect_e2e("ndarray_zeros", R"PURR(import io
32import ndarray
33io.print(ndarray.to_string(ndarray.zeros([2, 3])))
34)PURR", "[[0, 0, 0], [0, 0, 0]]\n");
37TEST(NdarrayCompileRun, Ones) {
38 e2e::expect_e2e("ndarray_ones", R"PURR(import io
39import ndarray
40io.print(ndarray.to_string(ndarray.ones([4])))
41)PURR", "[1, 1, 1, 1]\n");
44TEST(NdarrayCompileRun, Full) {
45 e2e::expect_e2e("ndarray_full", R"PURR(import io
46import ndarray
47io.print(ndarray.to_string(ndarray.full([2, 2], 7.0)))
48)PURR", "[[7, 7], [7, 7]]\n");
51TEST(NdarrayCompileRun, Arange) {
52 e2e::expect_e2e("ndarray_arange", R"PURR(import io
53import ndarray
54io.print(ndarray.to_string(ndarray.arange(0.0, 5.0, 1.0)))
55)PURR", "[0, 1, 2, 3, 4]\n");
58TEST(NdarrayCompileRun, Reshape) {
59 e2e::expect_e2e("ndarray_reshape", R"PURR(import io
60import ndarray
61io.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");
65TEST(NdarrayCompileRun, Add) {
66 e2e::expect_e2e("ndarray_add", R"PURR(import io
67import ndarray
68io.print(ndarray.to_string(ndarray.add(ndarray.array([1.0, 2.0, 3.0]), ndarray.scalar(10.0))))
69)PURR", "[11, 12, 13]\n");
72TEST(NdarrayCompileRun, Sub) {
73 e2e::expect_e2e("ndarray_sub", R"PURR(import io
74import ndarray
75io.print(ndarray.to_string(ndarray.sub(ndarray.array([5.0, 7.0, 9.0]), ndarray.scalar(1.0))))
76)PURR", "[4, 6, 8]\n");
79TEST(NdarrayCompileRun, Mul) {
80 e2e::expect_e2e("ndarray_mul", R"PURR(import io
81import ndarray
82io.print(ndarray.to_string(ndarray.mul(ndarray.array([2.0, 4.0, 6.0]), ndarray.scalar(0.5))))
83)PURR", "[1, 2, 3]\n");
86TEST(NdarrayCompileRun, Divide) {
87 e2e::expect_e2e("ndarray_divide", R"PURR(import io
88import ndarray
89io.print(ndarray.to_string(ndarray.divide(ndarray.array([2.0, 4.0, 6.0]), ndarray.scalar(2.0))))
90)PURR", "[1, 2, 3]\n");
93TEST(NdarrayCompileRun, Sum) {
94 e2e::expect_e2e("ndarray_sum", R"PURR(import io
95import ndarray
96io.print(ndarray.sum(ndarray.array([1.0, 2.0, 3.0, 4.0])))
97)PURR", "10\n");
100TEST(NdarrayCompileRun, Mean) {
101 e2e::expect_e2e("ndarray_mean", R"PURR(import io
102import ndarray
103io.print(ndarray.mean(ndarray.ones([4])))
104)PURR", "1\n");
107TEST(NdarrayCompileRun, Get) {
108 e2e::expect_e2e("ndarray_get", R"PURR(import io
109import ndarray
110io.print(ndarray.get(ndarray.array([1.0, 2.0, 3.0]), [2]))
111)PURR", "3\n");
114TEST(NdarrayCompileRun, ShapeOf) {
115 e2e::expect_e2e("ndarray_shape_of", R"PURR(import io
116import ndarray
117let s = ndarray.shape_of(ndarray.zeros([2, 3]))
118io.print(s[0], s[1])
119)PURR", "2 3\n");
122TEST(NdarrayCompileRun, SizeOf) {
123 e2e::expect_e2e("ndarray_size_of", R"PURR(import io
124import ndarray
125io.print(ndarray.size_of(ndarray.zeros([2, 3])))
126)PURR", "6\n");
129TEST(NdarrayCompileRun, ToString) {
130 e2e::expect_e2e("ndarray_to_string", R"PURR(import io
131import ndarray
132io.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");
136// io.print ABBREVIATES a large array with "..." (readable by default); a small one is full.
137TEST(NdarrayCompileRun, PrintAbbreviatesLargeArray) {
138 e2e::expect_e2e("ndarray_print_trunc", R"PURR(import io
139import ndarray
140io.print(ndarray.arange(0.0, 2000.0, 1.0))
141)PURR",
142 "[0, 1, 2, ..., 1997, 1998, 1999]\n");
145// io.rprint shows the array RAW (exactly as stored) — no abbreviation.
146TEST(NdarrayCompileRun, RprintShowsArrayFull) {
147 e2e::expect_e2e("ndarray_rprint_full", R"PURR(import io
148import ndarray
149io.rprint(ndarray.arange(0.0, 6.0, 1.0))
150)PURR",
151 "[0, 1, 2, 3, 4, 5]\n");
154TEST(NdarrayCompileRun, Complex) {
155 e2e::expect_e2e("ndarray_complex", R"PURR(import io
156import ndarray
157let z = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, -3.0]))
158io.print(ndarray.to_string(z))
159)PURR", "[0+1j, 2-3j]\n");
162TEST(NdarrayCompileRun, Conj) {
163 e2e::expect_e2e("ndarray_conj", R"PURR(import io
164import ndarray
165let z = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, -3.0]))
166io.print(ndarray.to_string(ndarray.conj(z)))
167)PURR", "[0-1j, 2+3j]\n");
170TEST(NdarrayCompileRun, Real) {
171 e2e::expect_e2e("ndarray_real", R"PURR(import io
172import ndarray
173let z = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, -3.0]))
174io.print(ndarray.to_string(ndarray.real(z)))
175)PURR", "[0, 2]\n");
178TEST(NdarrayCompileRun, Imag) {
179 e2e::expect_e2e("ndarray_imag", R"PURR(import io
180import ndarray
181let z = ndarray.complex(ndarray.array([0.0, 2.0]), ndarray.array([1.0, -3.0]))
182io.print(ndarray.to_string(ndarray.imag(z)))
183)PURR", "[1, -3]\n");
186TEST(NdarrayCompileRun, Sqrt) {
187 e2e::expect_e2e("ndarray_sqrt", R"PURR(import io
188import ndarray
189io.print(ndarray.to_string(ndarray.sqrt(ndarray.array([1.0, 4.0, 9.0, 16.0]))))
190)PURR", "[1, 2, 3, 4]\n");
193TEST(NdarrayCompileRun, Exp) {
194 e2e::expect_e2e("ndarray_exp", R"PURR(import io
195import ndarray
196io.print(ndarray.to_string(ndarray.exp(ndarray.array([0.0]))))
197)PURR", "[1]\n");
200TEST(NdarrayCompileRun, Sin) {
201 e2e::expect_e2e("ndarray_sin", R"PURR(import io
202import ndarray
203io.print(ndarray.to_string(ndarray.sin(ndarray.array([0.0]))))
204)PURR", "[0]\n");
207TEST(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 io
210import ndarray
211let m = ndarray.array([[1.0, 2.0], [3.0, 4.0]])
212io.print(ndarray.shape_of(m)[0], ndarray.shape_of(m)[1])
213io.print(ndarray.to_string(m))
214let t = ndarray.array([[[1.0], [2.0]], [[3.0], [4.0]]])
215io.print(ndarray.to_string(t))
216)PURR", "2 2\n[[1, 2], [3, 4]]\n[[[1], [2]], [[3], [4]]]\n");
219TEST(NdarrayCompileRun, Astype) {
220 // `arr.astype(<width>)` converts the element type — numpy's a.astype(dtype). The narrow
221 // element makes a smaller array (sizeof(i16) == 2), and i8/u8 elements print as NUMBERS.
222 e2e::expect_e2e("ndarray_astype", R"PURR(import io
223import ndarray
224let a = ndarray.array([1, 2, 3]).astype(i16)
225io.print(ndarray.to_string(a))
226io.print(sizeof(i16))
227let b = ndarray.array([65, 200, 9]).astype(u8)
228io.print(ndarray.to_string(b))
229let f = ndarray.array([1, 2, 3]).astype(f32)
230io.print(ndarray.to_string(f))
231)PURR", "[1, 2, 3]\n2\n[65, 200, 9]\n[1, 2, 3]\n");
234TEST(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 io
237import ndarray
238io.print(ndarray.to_string(ndarray.array([300, 256, 255]).astype(u8)))
239)PURR", "[44, 0, 255]\n");
242TEST(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, unsigned
245 // narrowing is modulo 2^bits, signed->unsigned same width reinterprets the bits, float->int
246 // 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 io
248import ndarray
249fn 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)))
258main()
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");
269// Both spellings of a width name reach astype identically, and a declared narrow type prints the
270// same sensible numbers as the explicit .astype form.
271TEST(NdarrayCompileRun, AstypeSpellingsAndDeclaredAgree) {
272 e2e::expect_e2e("ndarray_astype_spellings", R"PURR(import io
273import ndarray
274fn 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))
280main()
281)PURR", "[1, 2, 300]\n[1, 2, 300]\n[1, 2, 300]\n");
284TEST(NdarrayCompileRun, NarrowElementDeclaredTypeDrives) {
285 // A declared `ndarray<i8>` drives construction: the initializer is converted for you, so you
286 // 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 io
288import ndarray
289fn 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))
295main()
296)PURR", "[100, 101, 102]\n[[1, 2], [3, 4]]\n");