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>13
namespace b = cheatah::builtins;15
TEST(CheatahBuiltins, LenOrdChr) {16
EXPECT_EQ(b::len("meow"), 4u);17
EXPECT_EQ(b::ord("A"), 65);18
EXPECT_EQ(b::chr(65), "A");19
}21
TEST(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");30
}32
TEST(CheatahBuiltins, StrByteWidthIntsAreNumbers) {33
// i8/u8 (signed char / unsigned char) render as NUMBERS, not characters — the dedicated34
// overloads promote to a wider integer before to_string. Streamed as a raw char, 65 would35
// 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");40
}42
TEST(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");48
}50
TEST(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));58
}60
TEST(CheatahBuiltins, Ascii) {61
EXPECT_EQ(b::ascii("hi"), "'hi'");62
EXPECT_EQ(b::ascii(std::string("a\tb")), "'a\\x09b'");63
}65
TEST(CheatahBuiltins, Hash) {66
EXPECT_EQ(b::hash(std::string_view("meow")), b::hash(std::string_view("meow")));67
}69
TEST(CheatahBuiltins, ToFloatFromInt) {70
EXPECT_DOUBLE_EQ(b::to_float(7LL), 7.0);71
EXPECT_DOUBLE_EQ(b::to_float(-3LL), -3.0);72
}74
TEST(CheatahBuiltins, ToFloatFromFloat) {75
EXPECT_DOUBLE_EQ(b::to_float(0.95), 0.95); // identity — must NOT truncate via long long76
EXPECT_DOUBLE_EQ(b::to_float(-0.0169), -0.0169);77
}79
TEST(CheatahBuiltins, AsciiEscapesQuoteChar) {80
EXPECT_EQ(b::ascii("'"), "'\\''"); // a lone single quote -> \'81
}83
TEST(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 → \'86
}88
TEST(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);95
}97
TEST(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"));104
}106
TEST(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 end109
EXPECT_THROW(b::index(std::string("hi"), 5), std::out_of_range);110
}112
TEST(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);117
}119
TEST(CheatahBuiltins, IndexBoolList) {120
// std::vector<bool> is bit-packed (proxy references, no .data()), so it has121
// 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 end126
EXPECT_THROW(b::index(xs, 3), std::out_of_range);127
}129
TEST(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 end133
// of a list and asking for an entry that was never there are different mistakes, and `except e of134
// "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
}143
}145
TEST(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 start151
EXPECT_EQ(b::slice(s, 3, 1), ""); // empty when lo >= hi152
EXPECT_EQ(b::slice(s, 0, 100), s); // hi clamped to len153
}155
TEST(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());160
}162
TEST(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 -> float165
EXPECT_DOUBLE_EQ(b::truediv(6, 2), 3.0); // exact, but still a double166
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 adjust169
EXPECT_EQ(b::floordiv(-7, 2), -4); // different signs -> floor adjust170
EXPECT_EQ(b::floordiv(6, 2), 3); // exact (a%b == 0) -> no adjust171
EXPECT_DOUBLE_EQ(b::floordiv(7.0, 2.0), 3.0); // floating operands -> floored double172
EXPECT_DOUBLE_EQ(b::floordiv(7.0, 2), 3.0); // mixed -> floored double173
}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 itself177
// went untested; these pin the branch both ways plus the exact-division and same-sign no-adjust paths.178
TEST(CheatahBuiltins, Mod) {179
EXPECT_EQ(b::mod(7, 2), 1); // same sign -> no adjust180
EXPECT_EQ(b::mod(-7, 2), 1); // dividend negative, divisor positive -> +b correction181
EXPECT_EQ(b::mod(7, -2), -1); // dividend positive, divisor negative -> +b correction182
EXPECT_EQ(b::mod(-7, -2), -1); // both negative -> signs already agree, no adjust183
EXPECT_EQ(b::mod(6, 3), 0); // exact -> r == 0, no adjust184
EXPECT_EQ(b::mod(-6, 3), 0); // exact and negative -> still 0, must NOT become +3185
}187
// Integer `//` and `%` by zero raise a CONTROLLED error (std::domain_error) instead of undefined188
// behavior — C++ integer divide/modulo by zero is UB (SIGFPE). Float `/`,`//`,`%` are IEEE-safe.189
TEST(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);194
}196
// ord(): the code point of a one-byte char, unsigned (high bytes are 128..255, never negative).197
TEST(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 -1201
}203
// ---- errors: the kind/message value type behind `raise` and `except` -----------------------------205
TEST(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");214
}216
TEST(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");230
}232
TEST(CheatahBuiltins, CurrentErrorNormalizesEveryThrownType) {233
// The point of current_error: ONE handler shape covers everything that can arrive, including a234
// 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");250
}252
TEST(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 miss265
};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";276
}278
TEST(CheatahBuiltins, FinallySwallowsItsOwnThrowDuringUnwinding) {279
// A finally that throws WHILE an exception is unwinding would terminate the process. Losing the280
// 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
});289
}