Source
tests/purrc/lang_features_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
// End-to-end tests for the newer language features: break/continue, elif, match,4
// growable lists (append) + dict mutation, method-call syntax, and string/list5
// slicing with negative indices. Each writes a .purr, compiles it with purrc,6
// runs it under the cheatah runtime, and asserts the exact stdout.7
#include "e2e_harness.hpp"9
TEST(LangFeatures, BreakContinue) {10
e2e::expect_e2e("lang_break_continue", R"PURR(import io11
let sum = 012
for i in range(1, 20) {13
if i == 5 { continue }14
if i == 10 { break }15
sum = sum + i16
}17
io.print(sum)18
)PURR",19
"40\n"); // 1+2+3+4+6+7+8+920
}22
TEST(LangFeatures, ElifChain) {23
e2e::expect_e2e("lang_elif", R"PURR(import io24
fn classify(n) {25
if n < 0 { return "neg" }26
elif n == 0 { return "zero" }27
elif n < 10 { return "small" }28
else { return "big" }29
}30
for n in [-3, 0, 7, 99] {31
io.print(classify(n))32
}33
)PURR",34
"neg\nzero\nsmall\nbig\n");35
}37
TEST(LangFeatures, Match) {38
e2e::expect_e2e("lang_match", R"PURR(import io39
for x in [1, 2, 3] {40
match x {41
case 1 { io.print("one") }42
case 2 { io.print("two") }43
case _ { io.print("many") }44
}45
}46
)PURR",47
"one\ntwo\nmany\n");48
}50
TEST(LangFeatures, CompoundAssignment) {51
// += -= *= /= on ints, floats, and strings (lowers to the C++ operators).52
e2e::expect_e2e("lang_compound_assign", R"PURR(import io53
let n = 1054
n += 555
n -= 356
n *= 457
let x = 9.058
x /= 2.059
let s = "purr"60
s += "fect"61
io.print(n, x, s)62
)PURR", "48 4.5 purrfect\n");63
}65
TEST(LangFeatures, NdarrayOperators) {66
// Infix elementwise/scalar arithmetic and in-place compound assignment on67
// ndarrays: a * 2.0, a + b, a += b, a /= scalar.68
e2e::expect_e2e("lang_ndarray_operators", R"PURR(import io69
import ndarray70
let a = ndarray.array([1.0, 2.0, 3.0])71
let b = ndarray.array([10.0, 20.0, 30.0])72
io.print(ndarray.to_string(a * 2.0))73
io.print(ndarray.to_string(0.5 * b))74
io.print(ndarray.to_string(a + b))75
a += b76
a /= 11.077
io.print(ndarray.to_string(a))78
)PURR", "[2, 4, 6]\n[5, 10, 15]\n[11, 22, 33]\n[1, 2, 3]\n");79
}81
TEST(LangFeatures, ParamsPassByReference) {82
// Parameters bind by reference (Python object semantics): a function that83
// mutates a struct field, an ndarray element, or appends to a list through84
// its parameter changes the CALLER'S object. A literal argument still works85
// (binds as a temporary).86
e2e::expect_e2e("lang_byref_params", R"PURR(import io87
import ndarray89
struct Counter { hits: int }91
fn bump(c: Counter) {92
c.hits += 193
}95
fn scale_in_place(a, factor: float) {96
a *= factor97
}99
fn push_two(xs: list) {100
xs.append(2)101
}103
let c = Counter(0)104
bump(c)105
bump(c)106
let a = ndarray.array([1.0, 2.0])107
scale_in_place(a, 10.0)108
let xs = [1]109
push_two(xs)110
io.print(c.hits, ndarray.to_string(a), xs)111
)PURR", "2 [10, 20] [1, 2]\n");112
}114
TEST(LangFeatures, NdarraySubscript) {115
// x[i] and x[i, j] subscripts on ndarrays: reads, writes, negatives, and the116
// typed `: ndarray<int>` parameter spelling (in-place updates reach the caller).117
e2e::expect_e2e("lang_ndarray_subscript", R"PURR(import io118
import ndarray120
fn light_pixel(row : ndarray<int>) {121
row[0] = 1122
}124
let grid = ndarray.reshape(ndarray.array([1.0, 2.0, 3.0, 4.0]), [2, 2])125
grid[1, 0] = 9.0126
io.print(grid[1, 0], grid[0, 1], grid[-1, -1])127
let row = ndarray.array([0, 0, 0])128
light_pixel(row)129
row[2] = 5130
io.print(ndarray.to_string(row))131
)PURR", "9 2 4\n[1, 0, 5]\n");132
}134
TEST(LangFeatures, MatchWildcardWithAccumulator) {135
// Regression: a `case _` wildcard has no pattern expression; the dead-let136
// analysis scanning a match for reads of `acc` must not dereference it.137
e2e::expect_e2e("lang_match_wildcard_acc", R"PURR(import io138
fn grade(n: int) {139
let acc = 0.0140
match n {141
case 1 {142
acc += 1.0143
}144
case _ {145
}146
}147
return acc148
}149
io.print(grade(1), grade(7))150
)PURR", "1 0\n");151
}153
TEST(LangFeatures, AppendAndDictMutation) {154
e2e::expect_e2e("lang_append", R"PURR(import io155
let xs: list<int> = []156
append(xs, 1)157
xs.append(2)158
xs.append(3)159
io.print(len(xs), xs[2])160
let counts: dict<str, int> = {}161
counts["x"] = 1162
counts["x"] = counts["x"] + 5163
io.print(counts["x"])164
)PURR",165
"3 3\n6\n");166
}168
TEST(LangFeatures, MethodPredicates) {169
e2e::expect_e2e("lang_methods", R"PURR(import io170
io.print("</div>".startswith("</"))171
io.print("hello".endswith("lo"))172
io.print("abcd".contains("bc"))173
)PURR",174
"True\nTrue\nTrue\n");175
}177
TEST(LangFeatures, StringSlicingAndIndex) {178
e2e::expect_e2e("lang_slice_str", R"PURR(import io179
let s = "hello world"180
io.print(s[0], s[-1])181
io.print(s[0:5])182
io.print(s[6:])183
io.print(s[:5])184
io.print(s[-5:])185
io.print(s[0] == "h")186
)PURR",187
"h d\nhello\nworld\nhello\nworld\nTrue\n");188
}190
TEST(LangFeatures, ListSlicingAndIndex) {191
e2e::expect_e2e("lang_slice_list", R"PURR(import io192
let nums = [10, 20, 30, 40, 50]193
io.print(nums[-1], nums[1])194
let mid = nums[1:4]195
io.print(len(mid), mid[0], mid[2])196
)PURR",197
"50 20\n3 20 40\n");198
}200
// `xs[lo:hi] = rhs` replaces the range and resizes, as in Python. Every expected value here was201
// cross-checked against CPython.202
TEST(LangFeatures, ListSliceAssignment) {203
e2e::expect_e2e("lang_slice_assign", R"PURR(import io204
let same = [1, 2, 3, 4]205
same[1:3] = [9, 9]206
io.print(same)207
let grow = [1, 2, 3, 4]208
grow[1:3] = [7, 7, 7]209
io.print(grow)210
let shrink = [1, 2, 3, 4]211
shrink[1:3] = [5]212
io.print(shrink)213
let gone = [1, 2, 3, 4]214
gone[1:3] = []215
io.print(gone)216
let neg = [1, 2, 3, 4]217
neg[-3:-1] = [8]218
io.print(neg)219
let tail = [1, 2, 3, 4]220
tail[2:] = [6, 6]221
io.print(tail)222
let head = [1, 2, 3, 4]223
head[:2] = [0]224
io.print(head)225
)PURR",226
"[1, 9, 9, 4]\n[1, 7, 7, 7, 4]\n[1, 5, 4]\n[1, 4]\n[1, 8, 4]\n"227
"[1, 2, 6, 6]\n[0, 3, 4]\n");228
}230
// The source may be the destination itself, or a range inside it. Both are defined, and both231
// match CPython — the implementation copies the source before it disturbs the target.232
TEST(LangFeatures, ListSliceAssignmentAliasing) {233
e2e::expect_e2e("lang_slice_assign_alias", R"PURR(import io234
let a = [1, 2, 3, 4]235
a[1:3] = a236
io.print(a)237
let b = [1, 2, 3, 4]238
b[1:3] = b[0:2]239
io.print(b)240
)PURR",241
"[1, 1, 2, 3, 4, 4]\n[1, 1, 2, 4]\n");242
}244
// An ndarray assignment COPIES into the elements the slice addresses: the shape never changes,245
// a scalar broadcasts, and a slice read is a view onto the parent's storage.246
TEST(LangFeatures, NdarraySliceAssignment) {247
e2e::expect_e2e("lang_nd_slice_assign", R"PURR(import io248
import ndarray249
let a = ndarray.array([1.0, 2.0, 3.0, 4.0])250
a[1:3] = ndarray.array([9.0, 9.0])251
io.print(ndarray.to_string(a), ndarray.size_of(a))252
let b = ndarray.array([1.0, 2.0, 3.0, 4.0])253
b[1:3] = 7.0254
io.print(ndarray.to_string(b))255
let c = ndarray.array([1.0, 2.0, 3.0, 4.0])256
let v = c[1:3]257
v[0] = 99.0258
io.print(ndarray.to_string(c), ndarray.to_string(v))259
)PURR",260
"[1, 9, 9, 4] 4\n[1, 7, 7, 4]\n[1, 99, 3, 4] [99, 3]\n");261
}263
// A fixarray is the fixed-size array: a slice assignment fills it, and its extent cannot move.264
TEST(LangFeatures, FixarraySliceAssignment) {265
e2e::expect_e2e("lang_fx_slice_assign", R"PURR(import io266
import fixarray267
let v = fixarray.vec3f(1.0, 2.0, 3.0)268
v[0:2] = [9.0, 9.0]269
io.print(v[0], v[1], v[2])270
)PURR",271
"9 9 3\n");272
}274
// The optimizer must not be able to make a slice assignment disappear. Dead-local elimination275
// drops a `let` whose variable is never READ afterwards, and a slice assignment is the whole276
// reason a list may be written but never read again — so the same program is compiled with the277
// pass on and off, and both must print the same thing. docs/optimizations.md promises exactly278
// this: no transform changes observable behaviour.279
TEST(LangFeatures, SliceAssignmentSurvivesOptimization) {280
const std::string src = R"PURR(import io281
let xs = [1, 2, 3, 4]282
xs[1:3] = [9, 9]283
io.print(xs)284
)PURR";285
const std::string tmp = PURR_TEST_TMP;286
const std::string purr = tmp + "/lang_slice_opt.purr";287
{ std::ofstream f(purr); f << src; }288
for (const char* flags : {"", "--no-remove-variables", "--no-optimize-cpp"}) {289
const std::string mod = tmp + "/lang_slice_opt" + std::string(flags).substr(0, 6) + ".so";290
const std::string compile = std::string(PURRC_PATH) + " " + flags + " \"" + purr +291
"\" -o \"" + mod + "\"";292
ASSERT_EQ(std::system(compile.c_str()), 0) << "purrc failed with flags: " << flags;293
int rc = -1;294
const std::string out =295
e2e::run_capture("\"" + std::string(CHEATAH_RUNTIME_PATH) + "\" \"" + mod + "\"", rc);296
EXPECT_EQ(rc, 0) << flags;297
EXPECT_EQ(out, "[1, 9, 9, 4]\n") << "output changed with flags: " << flags;298
}299
// A list written ONLY through a slice assignment and never read must still compile: the300
// statement counts as a read of its target, so the binding it writes to survives.301
const std::string dead = tmp + "/lang_slice_dead.purr";302
{ std::ofstream f(dead); f << "let only = [1, 2, 3, 4]\nonly[1:3] = [9, 9]\n"; }303
const std::string mod = tmp + "/lang_slice_dead.so";304
EXPECT_EQ(std::system((std::string(PURRC_PATH) + " \"" + dead + "\" -o \"" + mod + "\"").c_str()), 0);305
}307
TEST(LangFeatures, ReturnTypeHints) {308
// Optional Python-style `-> Type` return hints. When present the function lowers with309
// that concrete C++ return type (the backend enforces it); when absent the return stays310
// `auto`. Mixed here: int, float, an ndarray, and an untyped function all interoperate.311
e2e::expect_e2e("lang_return_hints", R"PURR(import io312
import ndarray313
fn add(a : int, b : int) -> int {314
return a + b315
}316
fn half(x : float) -> float {317
return x / 2.0318
}319
fn untyped(x) {320
return x + 1321
}322
fn ones(n : int) -> ndarray<float> {323
let a = ndarray.zeros([n])324
for i in range(0, n) {325
a[i] = 1.0326
}327
return a328
}329
io.print(add(2, 3))330
io.print(half(9.0))331
io.print(untyped(41))332
io.print(ndarray.to_string(ones(3)))333
)PURR",334
"5\n4.5\n42\n[1, 1, 1]\n");335
}337
// Explicit template arguments on a call/construction, including NON-TYPE (integer-literal)338
// args: `f<3>(x)` and the mixed `f<int, 4>(x)`. A top-level cpp{} block supplies the C++339
// template fixtures so the program compiles AND runs; we also assert the emitted C++ carries340
// the literal (and the mapped type) verbatim. Guards the ambiguity with `<` as a comparison:341
// the args only commit when the angle list closes and is immediately followed by `(`.342
TEST(LangFeatures, NonTypeTemplateArgs) {343
const std::string gen = e2e::expect_e2e_source("lang_nontype_targs", R"PURR(import io344
cpp {345
template <long long N>346
long long times_n(long long x) { return x * N; }348
template <typename T, long long N>349
T scale_n(T x) { return x * static_cast<T>(N); }350
}351
io.print(times_n<3>(7))352
io.print(scale_n<int, 4>(5))353
)PURR",354
"21\n20\n");355
// The single non-type arg is spliced verbatim.356
EXPECT_NE(gen.find("times_n<3>"), std::string::npos)357
<< "expected the non-type template arg `<3>` in the emitted C++";358
// Mixed: the type arg maps (int -> long long) and the non-type literal passes through.359
EXPECT_NE(gen.find("scale_n<long long, 4>"), std::string::npos)360
<< "expected mixed `<long long, 4>` type + non-type args in the emitted C++";361
}363
// Compile-time `if constexpr (cond) {…}` — kept C++-style (parenthesised condition) and364
// lowered verbatim to C++ `if constexpr`, so the live branch is chosen at COMPILE time from365
// a constant condition. The constexpr-ness threads down the `else if constexpr` chain. A366
// cpp{} block supplies the constexpr fixtures so the program compiles AND runs; we also367
// assert the emitted C++ carries `if constexpr (` on both arms. Boolean logic (`and`,368
// comparisons) in the condition goes through the ordinary expression path.369
TEST(LangFeatures, IfConstexpr) {370
const std::string gen = e2e::expect_e2e_source("lang_if_constexpr", R"PURR(import io371
cpp {372
constexpr int kMode = 2;373
constexpr bool kOn = true;374
}375
fn pick() {376
if constexpr (kMode == 1 and kOn) {377
return "one"378
} else if constexpr (kMode == 2) {379
return "two"380
} else {381
return "other"382
}383
}384
io.print(pick())385
)PURR",386
"two\n");387
// The leading `if` lowers to a compile-time branch...388
const std::size_t first = gen.find("if constexpr (");389
ASSERT_NE(first, std::string::npos)390
<< "expected `if constexpr (` in the emitted C++";391
// ...and the `else if constexpr` arm inherits it (a second occurrence).392
EXPECT_NE(gen.find("if constexpr (", first + 1), std::string::npos)393
<< "the else-if arm should also lower to `if constexpr`";394
}396
// ============================================================================397
// Compile-time `constexpr` family — `constexpr let` / `constexpr fn` /398
// `constexpr match`, plus the AUTO-promotion of `if`/`match` over a known399
// constant, and the `match` -> `switch` vs `if/else-if` smart lowering.400
// These exercise the cases most likely to break the transpiler as it grows.401
// ============================================================================403
// `constexpr let` emits a C++ `constexpr` binding AND marks the name a compile-time404
// constant, so a plain `if` over it AUTO-lowers to `if constexpr`.405
TEST(LangFeatures, ConstexprLetAutoIf) {406
const std::string gen = e2e::expect_e2e_source("lang_cx_let_autoif", R"PURR(import io407
constexpr let N = 4408
if (N == 4) {409
io.print("four")410
} else {411
io.print("other")412
}413
)PURR",414
"four\n");415
EXPECT_NE(gen.find("constexpr auto N"), std::string::npos) << "let must emit `constexpr`";416
EXPECT_NE(gen.find("if constexpr ("), std::string::npos)417
<< "an `if` over a constexpr let must auto-lower to `if constexpr`";418
}420
// Constant-folding initializer + reference to an earlier constexpr let (chained constants).421
TEST(LangFeatures, ConstexprLetArithmeticAndChain) {422
const std::string gen = e2e::expect_e2e_source("lang_cx_let_chain", R"PURR(import io423
constexpr let A = 2 * 3 + 1424
constexpr let B = A + 5425
if (B == 12) { io.print("ok") } else { io.print("bad") }426
)PURR",427
"ok\n");428
EXPECT_NE(gen.find("constexpr auto A"), std::string::npos);429
EXPECT_NE(gen.find("constexpr auto B"), std::string::npos);430
EXPECT_NE(gen.find("if constexpr ("), std::string::npos);431
}433
// Explicit type annotation on a constexpr let still emits `constexpr`.434
TEST(LangFeatures, ConstexprLetTyped) {435
const std::string gen = e2e::expect_e2e_source("lang_cx_let_typed", R"PURR(import io436
constexpr let x: int = 5437
if (x < 10) { io.print("small") } else { io.print("big") }438
)PURR",439
"small\n");440
EXPECT_NE(gen.find("constexpr "), std::string::npos) << "typed constexpr let must emit `constexpr`";441
EXPECT_NE(gen.find("if constexpr ("), std::string::npos);442
}444
// Bool constexpr let drives an `if constexpr` with an `else` arm.445
TEST(LangFeatures, ConstexprLetBool) {446
e2e::expect_e2e("lang_cx_let_bool", R"PURR(import io447
constexpr let on = true448
if (on) { io.print("on") } else { io.print("off") }449
)PURR",450
"on\n");451
}453
// A purely-literal condition is itself a constant -> auto `if constexpr`, no `let` needed.454
TEST(LangFeatures, AutoIfLiteralCondition) {455
const std::string gen = e2e::expect_e2e_source("lang_cx_literal_if", R"PURR(import io456
if (1 + 1 == 2) { io.print("math") } else { io.print("broken") }457
)PURR",458
"math\n");459
EXPECT_NE(gen.find("if constexpr ("), std::string::npos);460
}462
// GUARD: a condition over a RUNTIME value (a function parameter) must stay a runtime `if`463
// — never auto-promoted (which would fail to compile, the value isn't constexpr).464
TEST(LangFeatures, RuntimeIfNotPromoted) {465
const std::string gen = e2e::expect_e2e_source("lang_cx_runtime_if", R"PURR(import io466
fn label(x: int) {467
if (x == 1) { return "one" } else { return "many" }468
}469
io.print(label(1), label(9))470
)PURR",471
"one many\n");472
EXPECT_EQ(gen.find("if constexpr"), std::string::npos)473
<< "an `if` over a runtime value must NOT be promoted to `if constexpr`";474
}476
// `constexpr fn`: a constexpr function whose call folds inside a `constexpr let`, and which477
// is ALSO callable at runtime.478
TEST(LangFeatures, ConstexprFn) {479
const std::string gen = e2e::expect_e2e_source("lang_cx_fn", R"PURR(import io480
constexpr fn square(x) { return x * x }481
constexpr let r = square(5)482
if (r == 25) { io.print("r25") } else { io.print("no") }483
io.print(square(3))484
)PURR",485
"r25\n9\n");486
EXPECT_NE(gen.find("constexpr auto square"), std::string::npos)487
<< "constexpr fn must emit a `constexpr` function";488
EXPECT_NE(gen.find("if constexpr ("), std::string::npos)489
<< "an `if` over the constexpr-folded result must auto-lower";490
}492
// Explicit `constexpr match` over a constant subject -> compile-time `if constexpr` chain.493
TEST(LangFeatures, ConstexprMatchExplicit) {494
const std::string gen = e2e::expect_e2e_source("lang_cx_match_explicit", R"PURR(import io495
constexpr let k = 2496
constexpr match k {497
case 1 { io.print("one") }498
case 2 { io.print("two") }499
case _ { io.print("other") }500
}501
)PURR",502
"two\n");503
EXPECT_NE(gen.find("if constexpr ("), std::string::npos)504
<< "constexpr match must lower to an `if constexpr` chain";505
EXPECT_EQ(gen.find("switch ("), std::string::npos)506
<< "constexpr match must NOT lower to a runtime switch";507
}509
// AUTO: a plain `match` over a constant subject with constant case labels also folds to the510
// compile-time `if constexpr` chain (no `constexpr` keyword on the match needed).511
TEST(LangFeatures, MatchAutoConstexpr) {512
const std::string gen = e2e::expect_e2e_source("lang_cx_match_auto", R"PURR(import io513
constexpr let k = 3514
match k {515
case 1 { io.print("a") }516
case 3 { io.print("c") }517
case _ { io.print("z") }518
}519
)PURR",520
"c\n");521
EXPECT_NE(gen.find("if constexpr ("), std::string::npos);522
}524
// A plain `match` on a RUNTIME integer lowers to a real C++ `switch` (default = `_`).525
TEST(LangFeatures, MatchRuntimeIntSwitch) {526
const std::string gen = e2e::expect_e2e_source("lang_match_switch", R"PURR(import io527
fn classify(n: int) {528
match n {529
case 1 { return "a" }530
case 2 { return "b" }531
case _ { return "c" }532
}533
}534
io.print(classify(1), classify(2), classify(9))535
)PURR",536
"a b c\n");537
EXPECT_NE(gen.find("switch ("), std::string::npos) << "integral match must lower to a switch";538
EXPECT_NE(gen.find("default:"), std::string::npos) << "the `_` case must be `default:`";539
EXPECT_EQ(gen.find("if constexpr"), std::string::npos);540
}542
// GUARD: a `match` on a STRING cannot be a switch (C++ forbids it) — it must fall back to the543
// `==` if/else-if chain. The most important correctness guard of the smart lowering.544
TEST(LangFeatures, MatchStringFallsBackToChain) {545
const std::string gen = e2e::expect_e2e_source("lang_match_string", R"PURR(import io546
fn code(s) {547
match s {548
case "red" { return 1 }549
case "green" { return 2 }550
case _ { return 0 }551
}552
}553
io.print(code("red"), code("green"), code("blue"))554
)PURR",555
"1 2 0\n");556
EXPECT_EQ(gen.find("switch ("), std::string::npos)557
<< "a string match must NOT lower to a switch (won't compile)";558
}560
// GUARD: bool case labels are not switch labels here either -> `==` chain.561
TEST(LangFeatures, MatchBoolFallsBackToChain) {562
const std::string gen = e2e::expect_e2e_source("lang_match_bool", R"PURR(import io563
fn name(b) {564
match b {565
case true { return "yes" }566
case false { return "no" }567
}568
}569
io.print(name(true), name(false))570
)PURR",571
"yes no\n");572
EXPECT_EQ(gen.find("switch ("), std::string::npos);573
}575
// A switch case whose body RETURNS must not emit an unreachable trailing `break` (else576
// -Wunreachable-code/-Werror could reject it). Verifies it compiles and runs.577
TEST(LangFeatures, MatchSwitchCaseReturnsNoUnreachableBreak) {578
e2e::expect_e2e("lang_match_return", R"PURR(import io579
fn pick(n: int) {580
match n {581
case 1 { return 10 }582
case 2 { return 20 }583
case _ { return 0 }584
}585
}586
io.print(pick(1), pick(2), pick(5))587
)PURR",588
"10 20 0\n");589
}591
// Negative integer case labels are valid switch labels (`case (-1):`).592
TEST(LangFeatures, MatchNegativeIntSwitch) {593
const std::string gen = e2e::expect_e2e_source("lang_match_negative", R"PURR(import io594
fn sign(n: int) {595
match n {596
case -1 { return "neg" }597
case 0 { return "zero" }598
case _ { return "pos" }599
}600
}601
io.print(sign(-1), sign(0), sign(7))602
)PURR",603
"neg zero pos\n");604
EXPECT_NE(gen.find("switch ("), std::string::npos);605
}607
// Identifiers that are legal cheatah names but C++ keywords (`delete`, `new`, `default`,608
// `switch`, `template`, `typename`, …) are emitted with a trailing `_` so the generated C++609
// compiles — applied symmetrically at declaration and every use site (fn/method/field/param/610
// var/loop/catch). cheatah's own keyword set is smaller (is_keyword, lexer.cpp), so these611
// stay valid source. String-literal contexts (a struct's printed field label) keep the612
// ORIGINAL spelling, not the escaped one.613
TEST(LangFeatures, CppKeywordIdentifiersAreEscaped) {614
const std::string gen = e2e::expect_e2e_source("lang_cpp_keyword_escape", R"PURR(import io615
struct Box {616
new: int617
default: str618
fn delete(self) { return self.new }619
}620
fn make(new) {621
return Box({.new = new, .default = "d"})622
}623
let b = make(5)624
io.print(b.delete())625
io.print(b.default)626
let switch = 3627
for template in range(0, switch) { io.print(template) }628
try { raise "boom" } except typename { io.print(typename) }629
)PURR",630
"5\nd\n0\n1\n2\nboom\n");631
// Declarations + use sites escaped: the keyword-named function, field, param, var, and632
// loop/catch variables all carry the trailing underscore in the emitted C++.633
EXPECT_NE(gen.find("delete_("), std::string::npos) << "method name not escaped:\n" << gen;634
EXPECT_NE(gen.find("new_"), std::string::npos) << "field/param name not escaped:\n" << gen;635
EXPECT_NE(gen.find("switch_"), std::string::npos) << "variable name not escaped:\n" << gen;636
EXPECT_NE(gen.find("template_"), std::string::npos) << "loop variable not escaped:\n" << gen;637
// The RAW keyword never appears as a bare C++ identifier (would not compile).638
EXPECT_EQ(gen.find(" delete("), std::string::npos) << "unescaped `delete(` leaked:\n" << gen;639
// A field's printed label keeps the ORIGINAL spelling (string literal, not an identifier).640
EXPECT_NE(gen.find("\"new"), std::string::npos) << "field label should keep raw name:\n" << gen;641
}643
// A program function may call one defined AFTER it, passing a program struct: the call644
// resolves by ADL on the argument type at instantiation. The emitted functions must645
// therefore live in the same namespace as the program's types — an anonymous namespace646
// around them (tried once for misc-use-anonymous-namespace) made this stop compiling in647
// a downstream app, and nothing in the corpus had exercised it.648
TEST(LangFeatures, ForwardReferenceThroughStructArgument) {649
e2e::expect_e2e("lang_forward_reference", R"PURR(import io650
struct Token {651
value: int652
}653
fn render(t: Token) {654
return describe(t)655
}656
fn describe(t: Token) {657
return "token " + str(t.value)658
}659
io.print(render(Token({.value = 7})))660
)PURR",661
"token 7\n");662
}