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>14
namespace b = cheatah::builtins;16
TEST(CheatahBuiltins, LenOrdChr) {17
EXPECT_EQ(b::len("meow"), 4u);18
EXPECT_EQ(b::ord("A"), 65);19
EXPECT_EQ(b::chr(65), "A");20
}22
TEST(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");31
}33
TEST(CheatahBuiltins, StrByteWidthIntsAreNumbers) {34
// i8/u8 (signed char / unsigned char) render as NUMBERS, not characters — the dedicated35
// overloads promote to a wider integer before to_string. Streamed as a raw char, 65 would36
// 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");41
}43
TEST(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");49
}51
TEST(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));59
}61
TEST(CheatahBuiltins, Ascii) {62
EXPECT_EQ(b::ascii("hi"), "'hi'");63
EXPECT_EQ(b::ascii(std::string("a\tb")), "'a\\x09b'");64
}66
TEST(CheatahBuiltins, Hash) {67
EXPECT_EQ(b::hash(std::string_view("meow")), b::hash(std::string_view("meow")));68
}70
TEST(CheatahBuiltins, ToFloatFromInt) {71
EXPECT_DOUBLE_EQ(b::to_float(7LL), 7.0);72
EXPECT_DOUBLE_EQ(b::to_float(-3LL), -3.0);73
}75
TEST(CheatahBuiltins, ToFloatFromFloat) {76
EXPECT_DOUBLE_EQ(b::to_float(0.95), 0.95); // identity — must NOT truncate via long long77
EXPECT_DOUBLE_EQ(b::to_float(-0.0169), -0.0169);78
}80
TEST(CheatahBuiltins, AsciiEscapesQuoteChar) {81
EXPECT_EQ(b::ascii("'"), "'\\''"); // a lone single quote -> \'82
}84
TEST(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 → \'87
}89
TEST(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);96
}98
TEST(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"));105
}107
TEST(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 end110
EXPECT_THROW(b::index(std::string("hi"), 5), std::out_of_range);111
}113
TEST(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);118
}120
TEST(CheatahBuiltins, IndexBoolList) {121
// std::vector<bool> is bit-packed (proxy references, no .data()), so it has122
// 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 end127
EXPECT_THROW(b::index(xs, 3), std::out_of_range);128
}130
TEST(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 end134
// of a list and asking for an entry that was never there are different mistakes, and `except e of135
// "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
}144
}146
TEST(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 start152
EXPECT_EQ(b::slice(s, 3, 1), ""); // empty when lo >= hi153
EXPECT_EQ(b::slice(s, 0, 100), s); // hi clamped to len154
}156
TEST(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());161
}163
// `xs[lo:hi] = rhs` replaces the range and RESIZES the list, exactly as Python does. Every164
// expectation below was cross-checked against CPython.165
TEST(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 place169
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}); // grows172
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}); // shrinks175
EXPECT_EQ(xs, (std::vector<long long>{1, 5, 4}));176
xs = v();177
b::slice_assign(xs, 1, 3, b::empty_seq{}); // deletes178
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 end181
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 range184
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 end187
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: append190
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 lo193
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 list196
EXPECT_EQ(empty, (std::vector<long long>{1, 2}));197
}199
// The source may BE the destination, or a range inside it. The implementation copies the source200
// into a temporary before erasing, so these are defined rather than reading through iterators the201
// erase has just invalidated. Both results match CPython.202
TEST(CheatahBuiltins, SliceAssignAliasing) {203
std::vector<long long> a{1, 2, 3, 4};204
b::slice_assign(a, 1, 3, a); // xs[1:3] = xs205
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 source208
EXPECT_EQ(c, (std::vector<long long>{1, 1, 2, 4}));209
}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.213
TEST(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 end218
EXPECT_EQ(v[2], 7);219
// a wrong-length source is refused rather than partially written220
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 delete223
EXPECT_THROW(b::slice_assign(v, 0, 2, b::empty_seq{}), std::runtime_error);224
}226
TEST(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 -> float229
EXPECT_DOUBLE_EQ(b::truediv(6, 2), 3.0); // exact, but still a double230
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 adjust233
EXPECT_EQ(b::floordiv(-7, 2), -4); // different signs -> floor adjust234
EXPECT_EQ(b::floordiv(6, 2), 3); // exact (a%b == 0) -> no adjust235
EXPECT_DOUBLE_EQ(b::floordiv(7.0, 2.0), 3.0); // floating operands -> floored double236
EXPECT_DOUBLE_EQ(b::floordiv(7.0, 2), 3.0); // mixed -> floored double237
}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 itself241
// went untested; these pin the branch both ways plus the exact-division and same-sign no-adjust paths.242
TEST(CheatahBuiltins, Mod) {243
EXPECT_EQ(b::mod(7, 2), 1); // same sign -> no adjust244
EXPECT_EQ(b::mod(-7, 2), 1); // dividend negative, divisor positive -> +b correction245
EXPECT_EQ(b::mod(7, -2), -1); // dividend positive, divisor negative -> +b correction246
EXPECT_EQ(b::mod(-7, -2), -1); // both negative -> signs already agree, no adjust247
EXPECT_EQ(b::mod(6, 3), 0); // exact -> r == 0, no adjust248
EXPECT_EQ(b::mod(-6, 3), 0); // exact and negative -> still 0, must NOT become +3249
}251
// Integer `//` and `%` by zero raise a CONTROLLED error (std::domain_error) instead of undefined252
// behavior — C++ integer divide/modulo by zero is UB (SIGFPE). Float `/`,`//`,`%` are IEEE-safe.253
TEST(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);258
}260
// ord(): the code point of a one-byte char, unsigned (high bytes are 128..255, never negative).261
TEST(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 -1265
}267
// ---- errors: the kind/message value type behind `raise` and `except` -----------------------------269
TEST(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");278
}280
TEST(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");294
}296
TEST(CheatahBuiltins, CurrentErrorNormalizesEveryThrownType) {297
// The point of current_error: ONE handler shape covers everything that can arrive, including a298
// 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");314
}316
TEST(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 miss329
};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 effect338
throw b::Error("x", "unwind");339
} catch (const b::Error&) {340
caught = true; // the throw exists only to unwind through g's scope341
}342
EXPECT_TRUE(caught);343
EXPECT_EQ(ran, 1) << "an exception unwinding through the scope";344
}346
TEST(CheatahBuiltins, FinallySwallowsItsOwnThrowDuringUnwinding) {347
// A finally that throws WHILE an exception is unwinding would terminate the process. Losing the348
// 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
});357
}