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 <vector>
11#include <gtest/gtest.h>
13namespace b = cheatah::builtins;
15TEST(CheatahBuiltins, LenOrdChr) {
16 EXPECT_EQ(b::len("meow"), 4u);
17 EXPECT_EQ(b::ord("A"), 65);
18 EXPECT_EQ(b::chr(65), "A");
21TEST(CheatahBuiltins, Str) {
22 // Streamable template: integers, floats (default 6-sig-digit form), and strings.
23 EXPECT_EQ(b::str(42LL), "42");
24 EXPECT_EQ(b::str(-7LL), "-7");
25 EXPECT_EQ(b::str(3.14), "3.14");
26 EXPECT_EQ(b::str(std::string("hi")), "hi");
27 // bool overload: Python's capitalized spelling, not 1/0.
28 EXPECT_EQ(b::str(true), "True");
29 EXPECT_EQ(b::str(false), "False");
32TEST(CheatahBuiltins, StrByteWidthIntsAreNumbers) {
33 // i8/u8 (signed char / unsigned char) render as NUMBERS, not characters — the dedicated
34 // overloads promote to a wider integer before to_string. Streamed as a raw char, 65 would
35 // print 'A'; here it must be "65".
36 EXPECT_EQ(b::str(static_cast<signed char>(65)), "65");
37 EXPECT_EQ(b::str(static_cast<signed char>(-5)), "-5");
38 EXPECT_EQ(b::str(static_cast<unsigned char>(200)), "200");
39 EXPECT_EQ(b::str(static_cast<unsigned char>(0)), "0");
42TEST(CheatahBuiltins, BaseReprs) {
43 EXPECT_EQ(b::hex(255), "0xff");
44 EXPECT_EQ(b::oct(8), "0o10");
45 EXPECT_EQ(b::bin(5), "0b101");
46 EXPECT_EQ(b::hex(-255), "-0xff");
47 EXPECT_EQ(b::hex(0), "0x0");
50TEST(CheatahBuiltins, Conversions) {
51 EXPECT_EQ(b::to_int("42"), 42);
52 EXPECT_EQ(b::to_int(3.9), 3);
53 EXPECT_DOUBLE_EQ(b::to_float("2.5"), 2.5);
54 EXPECT_TRUE(b::to_bool("x"));
55 EXPECT_FALSE(b::to_bool(""));
56 EXPECT_FALSE(b::to_bool(0));
57 EXPECT_TRUE(b::to_bool(7));
60TEST(CheatahBuiltins, Ascii) {
61 EXPECT_EQ(b::ascii("hi"), "'hi'");
62 EXPECT_EQ(b::ascii(std::string("a\tb")), "'a\\x09b'");
65TEST(CheatahBuiltins, Hash) {
66 EXPECT_EQ(b::hash(std::string_view("meow")), b::hash(std::string_view("meow")));
69TEST(CheatahBuiltins, ToFloatFromInt) {
70 EXPECT_DOUBLE_EQ(b::to_float(7LL), 7.0);
71 EXPECT_DOUBLE_EQ(b::to_float(-3LL), -3.0);
74TEST(CheatahBuiltins, ToFloatFromFloat) {
75 EXPECT_DOUBLE_EQ(b::to_float(0.95), 0.95); // identity — must NOT truncate via long long
76 EXPECT_DOUBLE_EQ(b::to_float(-0.0169), -0.0169);
79TEST(CheatahBuiltins, AsciiEscapesQuoteChar) {
80 EXPECT_EQ(b::ascii("'"), "'\\''"); // a lone single quote -> \'
83TEST(CheatahBuiltins, AsciiEscapesBackslashAndQuote) {
84 EXPECT_EQ(b::ascii(std::string("a\\b")), "'a\\\\b'"); // backslash → \\
85 EXPECT_EQ(b::ascii("it's"), "'it\\'s'"); // single quote → \'
88TEST(CheatahBuiltins, Append) {
89 std::vector<long long> xs;
90 b::append(xs, 1);
91 b::append(xs, 2LL);
92 ASSERT_EQ(xs.size(), 2u);
93 EXPECT_EQ(xs[0], 1);
94 EXPECT_EQ(xs[1], 2);
97TEST(CheatahBuiltins, StringPredicates) {
98 EXPECT_TRUE(b::startswith("</div>", "</"));
99 EXPECT_FALSE(b::startswith("x", "</"));
100 EXPECT_TRUE(b::endswith("hello", "lo"));
101 EXPECT_FALSE(b::endswith("hi", "lo"));
102 EXPECT_TRUE(b::contains("abcd", "bc"));
103 EXPECT_FALSE(b::contains("abcd", "zz"));
106TEST(CheatahBuiltins, IndexString) {
107 EXPECT_EQ(b::index(std::string("hello"), 0), "h");
108 EXPECT_EQ(b::index(std::string("hello"), -1), "o"); // negative from the end
109 EXPECT_THROW(b::index(std::string("hi"), 5), std::out_of_range);
112TEST(CheatahBuiltins, IndexList) {
113 const std::vector<long long> xs{10, 20, 30};
114 EXPECT_EQ(b::index(xs, 1), 20);
115 EXPECT_EQ(b::index(xs, -1), 30);
116 EXPECT_THROW(b::index(xs, 3), std::out_of_range);
119TEST(CheatahBuiltins, IndexBoolList) {
120 // std::vector<bool> is bit-packed (proxy references, no .data()), so it has
121 // its own index overload; semantics match every other sequence.
122 const std::vector<bool> xs{true, false, true};
123 EXPECT_TRUE(b::index(xs, 0));
124 EXPECT_FALSE(b::index(xs, 1));
125 EXPECT_TRUE(b::index(xs, -1)); // negative from the end
126 EXPECT_THROW(b::index(xs, 3), std::out_of_range);
129TEST(CheatahBuiltins, IndexDict) {
130 const std::unordered_map<std::string, long long> m{{"a", 1}, {"b", 2}};
131 EXPECT_EQ(b::index(m, std::string("a")), 1);
132 // A missing key raises kind "key", NOT the "index" a sequence subscript raises: walking off the end
133 // of a list and asking for an entry that was never there are different mistakes, and `except e of
134 // "key"` should be able to take one without silently swallowing the other.
135 EXPECT_THROW(b::index(m, std::string("z")), b::Error);
136 try {
137 b::index(m, std::string("z"));
138 FAIL() << "expected a raise";
139 } catch (const b::Error& e) {
140 EXPECT_EQ(e.kind(), b::kErrorKindKey);
141 EXPECT_EQ(e.message(), "key not found");
142 }
145TEST(CheatahBuiltins, SliceString) {
146 const std::string s = "hello world";
147 EXPECT_EQ(b::slice(s, 0, 5), "hello");
148 EXPECT_EQ(b::slice(s, 6, b::slice_end), "world"); // s[6:]
149 EXPECT_EQ(b::slice(s, 0, b::slice_end), s); // s[:]
150 EXPECT_EQ(b::slice(s, -5, b::slice_end), "world"); // negative start
151 EXPECT_EQ(b::slice(s, 3, 1), ""); // empty when lo >= hi
152 EXPECT_EQ(b::slice(s, 0, 100), s); // hi clamped to len
155TEST(CheatahBuiltins, SliceList) {
156 const std::vector<long long> xs{1, 2, 3, 4, 5};
157 EXPECT_EQ(b::slice(xs, 1, 4), (std::vector<long long>{2, 3, 4}));
158 EXPECT_EQ(b::slice(xs, -2, b::slice_end), (std::vector<long long>{4, 5}));
159 EXPECT_TRUE(b::slice(xs, 3, 1).empty());
162TEST(CheatahBuiltins, Division) {
163 // truediv (the `/` operator) is ALWAYS floating-point, like Python 3.
164 EXPECT_DOUBLE_EQ(b::truediv(6, 4), 1.5); // int / int -> float
165 EXPECT_DOUBLE_EQ(b::truediv(6, 2), 3.0); // exact, but still a double
166 EXPECT_DOUBLE_EQ(b::truediv(7.0, 2.0), 3.5);
167 // floordiv (the `//` operator) floors toward -inf, the way Python does.
168 EXPECT_EQ(b::floordiv(7, 2), 3); // a%b != 0, same sign -> no adjust
169 EXPECT_EQ(b::floordiv(-7, 2), -4); // different signs -> floor adjust
170 EXPECT_EQ(b::floordiv(6, 2), 3); // exact (a%b == 0) -> no adjust
171 EXPECT_DOUBLE_EQ(b::floordiv(7.0, 2.0), 3.0); // floating operands -> floored double
172 EXPECT_DOUBLE_EQ(b::floordiv(7.0, 2), 3.0); // mixed -> floored double
175// Integer `%` takes the sign of the DIVISOR (Python floor-mod), not of the dividend the way C++ does:
176// -7 % 2 is 1 here, not -1. Only the by-zero throw was covered before, so the sign-correction itself
177// went untested; these pin the branch both ways plus the exact-division and same-sign no-adjust paths.
178TEST(CheatahBuiltins, Mod) {
179 EXPECT_EQ(b::mod(7, 2), 1); // same sign -> no adjust
180 EXPECT_EQ(b::mod(-7, 2), 1); // dividend negative, divisor positive -> +b correction
181 EXPECT_EQ(b::mod(7, -2), -1); // dividend positive, divisor negative -> +b correction
182 EXPECT_EQ(b::mod(-7, -2), -1); // both negative -> signs already agree, no adjust
183 EXPECT_EQ(b::mod(6, 3), 0); // exact -> r == 0, no adjust
184 EXPECT_EQ(b::mod(-6, 3), 0); // exact and negative -> still 0, must NOT become +3
187// Integer `//` and `%` by zero raise a CONTROLLED error (std::domain_error) instead of undefined
188// behavior — C++ integer divide/modulo by zero is UB (SIGFPE). Float `/`,`//`,`%` are IEEE-safe.
189TEST(CheatahBuiltins, IntegerDivideAndModuloByZeroThrow) {
190 EXPECT_THROW(b::floordiv(1, 0), std::domain_error);
191 EXPECT_THROW(b::floordiv(-5, 0), std::domain_error);
192 EXPECT_THROW(b::mod(1, 0), std::domain_error);
193 EXPECT_THROW(b::mod(-5, 0), std::domain_error);
196// ord(): the code point of a one-byte char, unsigned (high bytes are 128..255, never negative).
197TEST(CheatahBuiltins, Ord) {
198 EXPECT_EQ(b::ord('A'), 65);
199 EXPECT_EQ(b::ord('0'), 48);
200 EXPECT_EQ(b::ord('\xff'), 255); // 0xFF -> 255, not -1
203// ---- errors: the kind/message value type behind `raise` and `except` -----------------------------
205TEST(CheatahBuiltins, ErrorCarriesKindAndMessage) {
206 const b::Error plain("boom");
207 EXPECT_EQ(plain.kind(), b::kErrorKindError) << "an unclassified raise gets the generic kind";
208 EXPECT_EQ(plain.message(), "boom");
209 EXPECT_STREQ(plain.what(), "boom") << "and it is still a std::exception carrying the message";
211 const b::Error classified("io", "disk full");
212 EXPECT_EQ(classified.kind(), "io");
213 EXPECT_EQ(classified.message(), "disk full");
216TEST(CheatahBuiltins, ErrorComparesAndPrintsAsItsMessage) {
217 const b::Error e("io", "disk full");
218 // All four orderings, both string and literal — this is what keeps `except e { if e == "..." }`
219 // reading the way it did when a handler bound a bare string.
220 EXPECT_TRUE(e == std::string("disk full"));
221 EXPECT_TRUE(std::string("disk full") == e);
222 EXPECT_TRUE(e == "disk full");
223 EXPECT_TRUE("disk full" == e);
224 EXPECT_FALSE(e == "io") << "comparison is against the MESSAGE, never the kind";
226 std::ostringstream os;
227 os << e;
228 EXPECT_EQ(os.str(), "disk full") << "streaming yields the sentence, not the kind";
229 EXPECT_EQ(b::str(e), "disk full");
232TEST(CheatahBuiltins, CurrentErrorNormalizesEveryThrownType) {
233 // The point of current_error: ONE handler shape covers everything that can arrive, including a
234 // type nothing knows about — which previously travelled past every handler and killed the process.
235 const auto caught = [](auto&& thrower) {
236 try {
237 thrower();
238 } catch (...) {
239 return b::current_error();
240 }
241 return b::Error("never", "never");
242 };
244 EXPECT_EQ(caught([] { throw b::Error("io", "passed through"); }).kind(), "io");
245 EXPECT_EQ(caught([] { throw std::out_of_range("oops"); }).kind(), b::kErrorKindIndex);
246 EXPECT_EQ(caught([] { throw std::domain_error("oops"); }).kind(), b::kErrorKindArithmetic);
247 EXPECT_EQ(caught([] { throw std::runtime_error("oops"); }).kind(), b::kErrorKindError);
248 EXPECT_EQ(caught([] { throw 42; }).kind(), b::kErrorKindUnknown) << "an int throw is still catchable";
249 EXPECT_EQ(caught([] { throw std::out_of_range("keep me"); }).message(), "keep me");
252TEST(CheatahBuiltins, FinallyRunsOnEveryExitPath) {
253 // A guard, not a duplicated block — so it survives the paths a duplicated block would skip.
254 int ran = 0;
256 {
257 auto g = b::make_finally([&] { ++ran; });
258 }
259 EXPECT_EQ(ran, 1) << "normal fall-through";
261 ran = 0;
262 const auto with_return = [&]() -> int {
263 auto g = b::make_finally([&] { ++ran; });
264 return 7; // the case a duplicated finally body would miss
265 };
266 EXPECT_EQ(with_return(), 7);
267 EXPECT_EQ(ran, 1) << "early return";
269 ran = 0;
270 try {
271 auto g = b::make_finally([&] { ++ran; });
272 throw b::Error("x", "unwind");
273 } catch (const b::Error&) {
274 }
275 EXPECT_EQ(ran, 1) << "an exception unwinding through the scope";
278TEST(CheatahBuiltins, FinallySwallowsItsOwnThrowDuringUnwinding) {
279 // A finally that throws WHILE an exception is unwinding would terminate the process. Losing the
280 // second error is the lesser harm, and this pins that choice so nobody "fixes" it into a crash.
281 EXPECT_NO_THROW({
282 try {
283 auto g = b::make_finally([] { throw std::runtime_error("from the guard"); });
284 throw b::Error("first", "the original");
285 } catch (const b::Error& e) {
286 EXPECT_EQ(e.message(), "the original") << "the original error is what survives";
287 }
288 });