cheatah
Source

stdlib/tests/builtins_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#include "builtins.hpp"
5#include <stdexcept>
6#include <sstream>
7#include <string>
8#include <unordered_map>
9#include <array>
10#include <vector>
12#include <gtest/gtest.h>
14namespace b = cheatah::builtins;
16TEST(CheatahBuiltins, LenOrdChr) {
17 EXPECT_EQ(b::len("meow"), 4u);
18 EXPECT_EQ(b::ord("A"), 65);
19 EXPECT_EQ(b::chr(65), "A");
22TEST(CheatahBuiltins, Str) {
23 // Streamable template: integers, floats (default 6-sig-digit form), and strings.
24 EXPECT_EQ(b::str(42LL), "42");
25 EXPECT_EQ(b::str(-7LL), "-7");
26 EXPECT_EQ(b::str(3.14), "3.14");
27 EXPECT_EQ(b::str(std::string("hi")), "hi");
28 // bool overload: Python's capitalized spelling, not 1/0.
29 EXPECT_EQ(b::str(true), "True");
30 EXPECT_EQ(b::str(false), "False");
33TEST(CheatahBuiltins, StrByteWidthIntsAreNumbers) {
34 // i8/u8 (signed char / unsigned char) render as NUMBERS, not characters — the dedicated
35 // overloads promote to a wider integer before to_string. Streamed as a raw char, 65 would
36 // print 'A'; here it must be "65".
37 EXPECT_EQ(b::str(static_cast<signed char>(65)), "65");
38 EXPECT_EQ(b::str(static_cast<signed char>(-5)), "-5");
39 EXPECT_EQ(b::str(static_cast<unsigned char>(200)), "200");
40 EXPECT_EQ(b::str(static_cast<unsigned char>(0)), "0");
43TEST(CheatahBuiltins, BaseReprs) {
44 EXPECT_EQ(b::hex(255), "0xff");
45 EXPECT_EQ(b::oct(8), "0o10");
46 EXPECT_EQ(b::bin(5), "0b101");
47 EXPECT_EQ(b::hex(-255), "-0xff");
48 EXPECT_EQ(b::hex(0), "0x0");
51TEST(CheatahBuiltins, Conversions) {
52 EXPECT_EQ(b::to_int("42"), 42);
53 EXPECT_EQ(b::to_int(3.9), 3);
54 EXPECT_DOUBLE_EQ(b::to_float("2.5"), 2.5);
55 EXPECT_TRUE(b::to_bool("x"));
56 EXPECT_FALSE(b::to_bool(""));
57 EXPECT_FALSE(b::to_bool(0));
58 EXPECT_TRUE(b::to_bool(7));
61TEST(CheatahBuiltins, Ascii) {
62 EXPECT_EQ(b::ascii("hi"), "'hi'");
63 EXPECT_EQ(b::ascii(std::string("a\tb")), "'a\\x09b'");
66TEST(CheatahBuiltins, Hash) {
67 EXPECT_EQ(b::hash(std::string_view("meow")), b::hash(std::string_view("meow")));
70TEST(CheatahBuiltins, ToFloatFromInt) {
71 EXPECT_DOUBLE_EQ(b::to_float(7LL), 7.0);
72 EXPECT_DOUBLE_EQ(b::to_float(-3LL), -3.0);
75TEST(CheatahBuiltins, ToFloatFromFloat) {
76 EXPECT_DOUBLE_EQ(b::to_float(0.95), 0.95); // identity — must NOT truncate via long long
77 EXPECT_DOUBLE_EQ(b::to_float(-0.0169), -0.0169);
80TEST(CheatahBuiltins, AsciiEscapesQuoteChar) {
81 EXPECT_EQ(b::ascii("'"), "'\\''"); // a lone single quote -> \'
84TEST(CheatahBuiltins, AsciiEscapesBackslashAndQuote) {
85 EXPECT_EQ(b::ascii(std::string("a\\b")), "'a\\\\b'"); // backslash → \\
86 EXPECT_EQ(b::ascii("it's"), "'it\\'s'"); // single quote → \'
89TEST(CheatahBuiltins, Append) {
90 std::vector<long long> xs;
91 b::append(xs, 1);
92 b::append(xs, 2LL);
93 ASSERT_EQ(xs.size(), 2u);
94 EXPECT_EQ(xs[0], 1);
95 EXPECT_EQ(xs[1], 2);
98TEST(CheatahBuiltins, StringPredicates) {
99 EXPECT_TRUE(b::startswith("</div>", "</"));
100 EXPECT_FALSE(b::startswith("x", "</"));
101 EXPECT_TRUE(b::endswith("hello", "lo"));
102 EXPECT_FALSE(b::endswith("hi", "lo"));
103 EXPECT_TRUE(b::contains("abcd", "bc"));
104 EXPECT_FALSE(b::contains("abcd", "zz"));
107TEST(CheatahBuiltins, IndexString) {
108 EXPECT_EQ(b::index(std::string("hello"), 0), "h");
109 EXPECT_EQ(b::index(std::string("hello"), -1), "o"); // negative from the end
110 EXPECT_THROW(b::index(std::string("hi"), 5), std::out_of_range);
113TEST(CheatahBuiltins, IndexList) {
114 const std::vector<long long> xs{10, 20, 30};
115 EXPECT_EQ(b::index(xs, 1), 20);
116 EXPECT_EQ(b::index(xs, -1), 30);
117 EXPECT_THROW(b::index(xs, 3), std::out_of_range);
120TEST(CheatahBuiltins, IndexBoolList) {
121 // std::vector<bool> is bit-packed (proxy references, no .data()), so it has
122 // its own index overload; semantics match every other sequence.
123 const std::vector<bool> xs{true, false, true};
124 EXPECT_TRUE(b::index(xs, 0));
125 EXPECT_FALSE(b::index(xs, 1));
126 EXPECT_TRUE(b::index(xs, -1)); // negative from the end
127 EXPECT_THROW(b::index(xs, 3), std::out_of_range);
130TEST(CheatahBuiltins, IndexDict) {
131 const std::unordered_map<std::string, long long> m{{"a", 1}, {"b", 2}};
132 EXPECT_EQ(b::index(m, std::string("a")), 1);
133 // A missing key raises kind "key", NOT the "index" a sequence subscript raises: walking off the end
134 // of a list and asking for an entry that was never there are different mistakes, and `except e of
135 // "key"` should be able to take one without silently swallowing the other.
136 EXPECT_THROW(b::index(m, std::string("z")), b::Error);
137 try {
138 b::index(m, std::string("z"));
139 FAIL() << "expected a raise";
140 } catch (const b::Error& e) {
141 EXPECT_EQ(e.kind(), b::kErrorKindKey);
142 EXPECT_EQ(e.message(), "key not found");
143 }
146TEST(CheatahBuiltins, SliceString) {
147 const std::string s = "hello world";
148 EXPECT_EQ(b::slice(s, 0, 5), "hello");
149 EXPECT_EQ(b::slice(s, 6, b::slice_end), "world"); // s[6:]
150 EXPECT_EQ(b::slice(s, 0, b::slice_end), s); // s[:]
151 EXPECT_EQ(b::slice(s, -5, b::slice_end), "world"); // negative start
152 EXPECT_EQ(b::slice(s, 3, 1), ""); // empty when lo >= hi
153 EXPECT_EQ(b::slice(s, 0, 100), s); // hi clamped to len
156TEST(CheatahBuiltins, SliceList) {
157 const std::vector<long long> xs{1, 2, 3, 4, 5};
158 EXPECT_EQ(b::slice(xs, 1, 4), (std::vector<long long>{2, 3, 4}));
159 EXPECT_EQ(b::slice(xs, -2, b::slice_end), (std::vector<long long>{4, 5}));
160 EXPECT_TRUE(b::slice(xs, 3, 1).empty());
163// `xs[lo:hi] = rhs` replaces the range and RESIZES the list, exactly as Python does. Every
164// expectation below was cross-checked against CPython.
165TEST(CheatahBuiltins, SliceAssignList) {
166 auto v = [] { return std::vector<long long>{1, 2, 3, 4}; };
167 std::vector<long long> xs = v();
168 b::slice_assign(xs, 1, 3, std::vector<long long>{9, 9}); // same length: in place
169 EXPECT_EQ(xs, (std::vector<long long>{1, 9, 9, 4}));
170 xs = v();
171 b::slice_assign(xs, 1, 3, std::vector<long long>{7, 7, 7}); // grows
172 EXPECT_EQ(xs, (std::vector<long long>{1, 7, 7, 7, 4}));
173 xs = v();
174 b::slice_assign(xs, 1, 3, std::vector<long long>{5}); // shrinks
175 EXPECT_EQ(xs, (std::vector<long long>{1, 5, 4}));
176 xs = v();
177 b::slice_assign(xs, 1, 3, b::empty_seq{}); // deletes
178 EXPECT_EQ(xs, (std::vector<long long>{1, 4}));
179 xs = v();
180 b::slice_assign(xs, -3, -1, std::vector<long long>{8}); // negatives from the end
181 EXPECT_EQ(xs, (std::vector<long long>{1, 8, 4}));
182 xs = v();
183 b::slice_assign(xs, 0, 2, std::vector<long long>{0}); // leading range
184 EXPECT_EQ(xs, (std::vector<long long>{0, 3, 4}));
185 xs = v();
186 b::slice_assign(xs, 2, b::slice_end, std::vector<long long>{6, 6}); // to the end
187 EXPECT_EQ(xs, (std::vector<long long>{1, 2, 6, 6}));
188 xs = v();
189 b::slice_assign(xs, 10, 20, std::vector<long long>{5}); // out of range clamps: append
190 EXPECT_EQ(xs, (std::vector<long long>{1, 2, 3, 4, 5}));
191 xs = v();
192 b::slice_assign(xs, 3, 1, std::vector<long long>{9}); // reversed: insert at lo
193 EXPECT_EQ(xs, (std::vector<long long>{1, 2, 3, 9, 4}));
194 std::vector<long long> empty;
195 b::slice_assign(empty, 0, 0, std::vector<long long>{1, 2}); // into an empty list
196 EXPECT_EQ(empty, (std::vector<long long>{1, 2}));
199// The source may BE the destination, or a range inside it. The implementation copies the source
200// into a temporary before erasing, so these are defined rather than reading through iterators the
201// erase has just invalidated. Both results match CPython.
202TEST(CheatahBuiltins, SliceAssignAliasing) {
203 std::vector<long long> a{1, 2, 3, 4};
204 b::slice_assign(a, 1, 3, a); // xs[1:3] = xs
205 EXPECT_EQ(a, (std::vector<long long>{1, 1, 2, 3, 4, 4}));
206 std::vector<long long> c{1, 2, 3, 4};
207 b::slice_assign(c, 1, 3, b::slice(c, 0, 2)); // overlapping source
208 EXPECT_EQ(c, (std::vector<long long>{1, 1, 2, 4}));
211// A fixed-size `array<T, N>` is FILLED by a slice assignment, exactly as fixarray and ndarray are:
212// the values are copied into storage it already owns and the extent never moves.
213TEST(CheatahBuiltins, SliceAssignFixedArray) {
214 std::array<long long, 4> v{1, 2, 3, 4};
215 b::slice_assign(v, 1, 3, std::vector<long long>{9, 9});
216 EXPECT_EQ(v, (std::array<long long, 4>{1, 9, 9, 4}));
217 b::slice_assign(v, -2, -1, std::vector<long long>{7}); // negatives count from the end
218 EXPECT_EQ(v[2], 7);
219 // a wrong-length source is refused rather than partially written
220 EXPECT_THROW(b::slice_assign(v, 0, 2, std::vector<long long>{1}), std::runtime_error);
221 EXPECT_EQ(v[0], 1);
222 // and a fixed extent has nothing to delete
223 EXPECT_THROW(b::slice_assign(v, 0, 2, b::empty_seq{}), std::runtime_error);
226TEST(CheatahBuiltins, Division) {
227 // truediv (the `/` operator) is ALWAYS floating-point, like Python 3.
228 EXPECT_DOUBLE_EQ(b::truediv(6, 4), 1.5); // int / int -> float
229 EXPECT_DOUBLE_EQ(b::truediv(6, 2), 3.0); // exact, but still a double
230 EXPECT_DOUBLE_EQ(b::truediv(7.0, 2.0), 3.5);
231 // floordiv (the `//` operator) floors toward -inf, the way Python does.
232 EXPECT_EQ(b::floordiv(7, 2), 3); // a%b != 0, same sign -> no adjust
233 EXPECT_EQ(b::floordiv(-7, 2), -4); // different signs -> floor adjust
234 EXPECT_EQ(b::floordiv(6, 2), 3); // exact (a%b == 0) -> no adjust
235 EXPECT_DOUBLE_EQ(b::floordiv(7.0, 2.0), 3.0); // floating operands -> floored double
236 EXPECT_DOUBLE_EQ(b::floordiv(7.0, 2), 3.0); // mixed -> floored double
239// Integer `%` takes the sign of the DIVISOR (Python floor-mod), not of the dividend the way C++ does:
240// -7 % 2 is 1 here, not -1. Only the by-zero throw was covered before, so the sign-correction itself
241// went untested; these pin the branch both ways plus the exact-division and same-sign no-adjust paths.
242TEST(CheatahBuiltins, Mod) {
243 EXPECT_EQ(b::mod(7, 2), 1); // same sign -> no adjust
244 EXPECT_EQ(b::mod(-7, 2), 1); // dividend negative, divisor positive -> +b correction
245 EXPECT_EQ(b::mod(7, -2), -1); // dividend positive, divisor negative -> +b correction
246 EXPECT_EQ(b::mod(-7, -2), -1); // both negative -> signs already agree, no adjust
247 EXPECT_EQ(b::mod(6, 3), 0); // exact -> r == 0, no adjust
248 EXPECT_EQ(b::mod(-6, 3), 0); // exact and negative -> still 0, must NOT become +3
251// Integer `//` and `%` by zero raise a CONTROLLED error (std::domain_error) instead of undefined
252// behavior — C++ integer divide/modulo by zero is UB (SIGFPE). Float `/`,`//`,`%` are IEEE-safe.
253TEST(CheatahBuiltins, IntegerDivideAndModuloByZeroThrow) {
254 EXPECT_THROW(b::floordiv(1, 0), std::domain_error);
255 EXPECT_THROW(b::floordiv(-5, 0), std::domain_error);
256 EXPECT_THROW(b::mod(1, 0), std::domain_error);
257 EXPECT_THROW(b::mod(-5, 0), std::domain_error);
260// ord(): the code point of a one-byte char, unsigned (high bytes are 128..255, never negative).
261TEST(CheatahBuiltins, Ord) {
262 EXPECT_EQ(b::ord('A'), 65);
263 EXPECT_EQ(b::ord('0'), 48);
264 EXPECT_EQ(b::ord('\xff'), 255); // 0xFF -> 255, not -1
267// ---- errors: the kind/message value type behind `raise` and `except` -----------------------------
269TEST(CheatahBuiltins, ErrorCarriesKindAndMessage) {
270 const b::Error plain("boom");
271 EXPECT_EQ(plain.kind(), b::kErrorKindError) << "an unclassified raise gets the generic kind";
272 EXPECT_EQ(plain.message(), "boom");
273 EXPECT_STREQ(plain.what(), "boom") << "and it is still a std::exception carrying the message";
275 const b::Error classified("io", "disk full");
276 EXPECT_EQ(classified.kind(), "io");
277 EXPECT_EQ(classified.message(), "disk full");
280TEST(CheatahBuiltins, ErrorComparesAndPrintsAsItsMessage) {
281 const b::Error e("io", "disk full");
282 // All four orderings, both string and literal — this is what keeps `except e { if e == "..." }`
283 // reading the way it did when a handler bound a bare string.
284 EXPECT_TRUE(e == std::string("disk full"));
285 EXPECT_TRUE(std::string("disk full") == e);
286 EXPECT_TRUE(e == "disk full");
287 EXPECT_TRUE("disk full" == e);
288 EXPECT_FALSE(e == "io") << "comparison is against the MESSAGE, never the kind";
290 std::ostringstream os;
291 os << e;
292 EXPECT_EQ(os.str(), "disk full") << "streaming yields the sentence, not the kind";
293 EXPECT_EQ(b::str(e), "disk full");
296TEST(CheatahBuiltins, CurrentErrorNormalizesEveryThrownType) {
297 // The point of current_error: ONE handler shape covers everything that can arrive, including a
298 // type nothing knows about — which previously travelled past every handler and killed the process.
299 const auto caught = [](auto&& thrower) {
300 try {
301 thrower();
302 } catch (...) {
303 return b::current_error();
304 }
305 return b::Error("never", "never");
306 };
308 EXPECT_EQ(caught([] { throw b::Error("io", "passed through"); }).kind(), "io");
309 EXPECT_EQ(caught([] { throw std::out_of_range("oops"); }).kind(), b::kErrorKindIndex);
310 EXPECT_EQ(caught([] { throw std::domain_error("oops"); }).kind(), b::kErrorKindArithmetic);
311 EXPECT_EQ(caught([] { throw std::runtime_error("oops"); }).kind(), b::kErrorKindError);
312 EXPECT_EQ(caught([] { throw 42; }).kind(), b::kErrorKindUnknown) << "an int throw is still catchable";
313 EXPECT_EQ(caught([] { throw std::out_of_range("keep me"); }).message(), "keep me");
316TEST(CheatahBuiltins, FinallyRunsOnEveryExitPath) {
317 // A guard, not a duplicated block — so it survives the paths a duplicated block would skip.
318 int ran = 0;
320 {
321 auto g = b::make_finally([&] { ++ran; });
322 }
323 EXPECT_EQ(ran, 1) << "normal fall-through";
325 ran = 0;
326 const auto with_return = [&]() -> int {
327 auto g = b::make_finally([&] { ++ran; });
328 return 7; // the case a duplicated finally body would miss
329 };
330 EXPECT_EQ(with_return(), 7);
331 EXPECT_EQ(ran, 1) << "early return";
333 ran = 0;
334 bool caught = false;
335 try {
336 const auto g = b::make_finally([&] { ++ran; });
337 static_cast<void>(g); // held only for its scope-exit effect
338 throw b::Error("x", "unwind");
339 } catch (const b::Error&) {
340 caught = true; // the throw exists only to unwind through g's scope
341 }
342 EXPECT_TRUE(caught);
343 EXPECT_EQ(ran, 1) << "an exception unwinding through the scope";
346TEST(CheatahBuiltins, FinallySwallowsItsOwnThrowDuringUnwinding) {
347 // A finally that throws WHILE an exception is unwinding would terminate the process. Losing the
348 // second error is the lesser harm, and this pins that choice so nobody "fixes" it into a crash.
349 EXPECT_NO_THROW({
350 try {
351 auto g = b::make_finally([] { throw std::runtime_error("from the guard"); });
352 throw b::Error("first", "the original");
353 } catch (const b::Error& e) {
354 EXPECT_EQ(e.message(), "the original") << "the original error is what survives";
355 }
356 });