cheatah
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/list
5// 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"
9TEST(LangFeatures, BreakContinue) {
10 e2e::expect_e2e("lang_break_continue", R"PURR(import io
11let sum = 0
12for i in range(1, 20) {
13 if i == 5 { continue }
14 if i == 10 { break }
15 sum = sum + i
17io.print(sum)
18)PURR",
19 "40\n"); // 1+2+3+4+6+7+8+9
22TEST(LangFeatures, ElifChain) {
23 e2e::expect_e2e("lang_elif", R"PURR(import io
24fn classify(n) {
25 if n < 0 { return "neg" }
26 elif n == 0 { return "zero" }
27 elif n < 10 { return "small" }
28 else { return "big" }
30for n in [-3, 0, 7, 99] {
31 io.print(classify(n))
33)PURR",
34 "neg\nzero\nsmall\nbig\n");
37TEST(LangFeatures, Match) {
38 e2e::expect_e2e("lang_match", R"PURR(import io
39for 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 }
46)PURR",
47 "one\ntwo\nmany\n");
50TEST(LangFeatures, CompoundAssignment) {
51 // += -= *= /= on ints, floats, and strings (lowers to the C++ operators).
52 e2e::expect_e2e("lang_compound_assign", R"PURR(import io
53let n = 10
54n += 5
55n -= 3
56n *= 4
57let x = 9.0
58x /= 2.0
59let s = "purr"
60s += "fect"
61io.print(n, x, s)
62)PURR", "48 4.5 purrfect\n");
65TEST(LangFeatures, NdarrayOperators) {
66 // Infix elementwise/scalar arithmetic and in-place compound assignment on
67 // ndarrays: a * 2.0, a + b, a += b, a /= scalar.
68 e2e::expect_e2e("lang_ndarray_operators", R"PURR(import io
69import ndarray
70let a = ndarray.array([1.0, 2.0, 3.0])
71let b = ndarray.array([10.0, 20.0, 30.0])
72io.print(ndarray.to_string(a * 2.0))
73io.print(ndarray.to_string(0.5 * b))
74io.print(ndarray.to_string(a + b))
75a += b
76a /= 11.0
77io.print(ndarray.to_string(a))
78)PURR", "[2, 4, 6]\n[5, 10, 15]\n[11, 22, 33]\n[1, 2, 3]\n");
81TEST(LangFeatures, ParamsPassByReference) {
82 // Parameters bind by reference (Python object semantics): a function that
83 // mutates a struct field, an ndarray element, or appends to a list through
84 // its parameter changes the CALLER'S object. A literal argument still works
85 // (binds as a temporary).
86 e2e::expect_e2e("lang_byref_params", R"PURR(import io
87import ndarray
89struct Counter { hits: int }
91fn bump(c: Counter) {
92 c.hits += 1
95fn scale_in_place(a, factor: float) {
96 a *= factor
99fn push_two(xs: list) {
100 xs.append(2)
103let c = Counter(0)
104bump(c)
105bump(c)
106let a = ndarray.array([1.0, 2.0])
107scale_in_place(a, 10.0)
108let xs = [1]
109push_two(xs)
110io.print(c.hits, ndarray.to_string(a), xs)
111)PURR", "2 [10, 20] [1, 2]\n");
114TEST(LangFeatures, NdarraySubscript) {
115 // x[i] and x[i, j] subscripts on ndarrays: reads, writes, negatives, and the
116 // typed `: ndarray<int>` parameter spelling (in-place updates reach the caller).
117 e2e::expect_e2e("lang_ndarray_subscript", R"PURR(import io
118import ndarray
120fn light_pixel(row : ndarray<int>) {
121 row[0] = 1
124let grid = ndarray.reshape(ndarray.array([1.0, 2.0, 3.0, 4.0]), [2, 2])
125grid[1, 0] = 9.0
126io.print(grid[1, 0], grid[0, 1], grid[-1, -1])
127let row = ndarray.array([0, 0, 0])
128light_pixel(row)
129row[2] = 5
130io.print(ndarray.to_string(row))
131)PURR", "9 2 4\n[1, 0, 5]\n");
134TEST(LangFeatures, MatchWildcardWithAccumulator) {
135 // Regression: a `case _` wildcard has no pattern expression; the dead-let
136 // analysis scanning a match for reads of `acc` must not dereference it.
137 e2e::expect_e2e("lang_match_wildcard_acc", R"PURR(import io
138fn grade(n: int) {
139 let acc = 0.0
140 match n {
141 case 1 {
142 acc += 1.0
143 }
144 case _ {
145 }
146 }
147 return acc
149io.print(grade(1), grade(7))
150)PURR", "1 0\n");
153TEST(LangFeatures, AppendAndDictMutation) {
154 e2e::expect_e2e("lang_append", R"PURR(import io
155let xs: list<int> = []
156append(xs, 1)
157xs.append(2)
158xs.append(3)
159io.print(len(xs), xs[2])
160let counts: dict<str, int> = {}
161counts["x"] = 1
162counts["x"] = counts["x"] + 5
163io.print(counts["x"])
164)PURR",
165 "3 3\n6\n");
168TEST(LangFeatures, MethodPredicates) {
169 e2e::expect_e2e("lang_methods", R"PURR(import io
170io.print("</div>".startswith("</"))
171io.print("hello".endswith("lo"))
172io.print("abcd".contains("bc"))
173)PURR",
174 "True\nTrue\nTrue\n");
177TEST(LangFeatures, StringSlicingAndIndex) {
178 e2e::expect_e2e("lang_slice_str", R"PURR(import io
179let s = "hello world"
180io.print(s[0], s[-1])
181io.print(s[0:5])
182io.print(s[6:])
183io.print(s[:5])
184io.print(s[-5:])
185io.print(s[0] == "h")
186)PURR",
187 "h d\nhello\nworld\nhello\nworld\nTrue\n");
190TEST(LangFeatures, ListSlicingAndIndex) {
191 e2e::expect_e2e("lang_slice_list", R"PURR(import io
192let nums = [10, 20, 30, 40, 50]
193io.print(nums[-1], nums[1])
194let mid = nums[1:4]
195io.print(len(mid), mid[0], mid[2])
196)PURR",
197 "50 20\n3 20 40\n");
200// `xs[lo:hi] = rhs` replaces the range and resizes, as in Python. Every expected value here was
201// cross-checked against CPython.
202TEST(LangFeatures, ListSliceAssignment) {
203 e2e::expect_e2e("lang_slice_assign", R"PURR(import io
204let same = [1, 2, 3, 4]
205same[1:3] = [9, 9]
206io.print(same)
207let grow = [1, 2, 3, 4]
208grow[1:3] = [7, 7, 7]
209io.print(grow)
210let shrink = [1, 2, 3, 4]
211shrink[1:3] = [5]
212io.print(shrink)
213let gone = [1, 2, 3, 4]
214gone[1:3] = []
215io.print(gone)
216let neg = [1, 2, 3, 4]
217neg[-3:-1] = [8]
218io.print(neg)
219let tail = [1, 2, 3, 4]
220tail[2:] = [6, 6]
221io.print(tail)
222let head = [1, 2, 3, 4]
223head[:2] = [0]
224io.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");
230// The source may be the destination itself, or a range inside it. Both are defined, and both
231// match CPython — the implementation copies the source before it disturbs the target.
232TEST(LangFeatures, ListSliceAssignmentAliasing) {
233 e2e::expect_e2e("lang_slice_assign_alias", R"PURR(import io
234let a = [1, 2, 3, 4]
235a[1:3] = a
236io.print(a)
237let b = [1, 2, 3, 4]
238b[1:3] = b[0:2]
239io.print(b)
240)PURR",
241 "[1, 1, 2, 3, 4, 4]\n[1, 1, 2, 4]\n");
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.
246TEST(LangFeatures, NdarraySliceAssignment) {
247 e2e::expect_e2e("lang_nd_slice_assign", R"PURR(import io
248import ndarray
249let a = ndarray.array([1.0, 2.0, 3.0, 4.0])
250a[1:3] = ndarray.array([9.0, 9.0])
251io.print(ndarray.to_string(a), ndarray.size_of(a))
252let b = ndarray.array([1.0, 2.0, 3.0, 4.0])
253b[1:3] = 7.0
254io.print(ndarray.to_string(b))
255let c = ndarray.array([1.0, 2.0, 3.0, 4.0])
256let v = c[1:3]
257v[0] = 99.0
258io.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");
263// A fixarray is the fixed-size array: a slice assignment fills it, and its extent cannot move.
264TEST(LangFeatures, FixarraySliceAssignment) {
265 e2e::expect_e2e("lang_fx_slice_assign", R"PURR(import io
266import fixarray
267let v = fixarray.vec3f(1.0, 2.0, 3.0)
268v[0:2] = [9.0, 9.0]
269io.print(v[0], v[1], v[2])
270)PURR",
271 "9 9 3\n");
274// The optimizer must not be able to make a slice assignment disappear. Dead-local elimination
275// drops a `let` whose variable is never READ afterwards, and a slice assignment is the whole
276// reason a list may be written but never read again — so the same program is compiled with the
277// pass on and off, and both must print the same thing. docs/optimizations.md promises exactly
278// this: no transform changes observable behaviour.
279TEST(LangFeatures, SliceAssignmentSurvivesOptimization) {
280 const std::string src = R"PURR(import io
281let xs = [1, 2, 3, 4]
282xs[1:3] = [9, 9]
283io.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: the
300 // 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);
307TEST(LangFeatures, ReturnTypeHints) {
308 // Optional Python-style `-> Type` return hints. When present the function lowers with
309 // that concrete C++ return type (the backend enforces it); when absent the return stays
310 // `auto`. Mixed here: int, float, an ndarray, and an untyped function all interoperate.
311 e2e::expect_e2e("lang_return_hints", R"PURR(import io
312import ndarray
313fn add(a : int, b : int) -> int {
314 return a + b
316fn half(x : float) -> float {
317 return x / 2.0
319fn untyped(x) {
320 return x + 1
322fn ones(n : int) -> ndarray<float> {
323 let a = ndarray.zeros([n])
324 for i in range(0, n) {
325 a[i] = 1.0
326 }
327 return a
329io.print(add(2, 3))
330io.print(half(9.0))
331io.print(untyped(41))
332io.print(ndarray.to_string(ones(3)))
333)PURR",
334 "5\n4.5\n42\n[1, 1, 1]\n");
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++ carries
340// 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 `(`.
342TEST(LangFeatures, NonTypeTemplateArgs) {
343 const std::string gen = e2e::expect_e2e_source("lang_nontype_targs", R"PURR(import io
344cpp {
345template <long long N>
346long long times_n(long long x) { return x * N; }
348template <typename T, long long N>
349T scale_n(T x) { return x * static_cast<T>(N); }
351io.print(times_n<3>(7))
352io.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++";
363// Compile-time `if constexpr (cond) {…}` — kept C++-style (parenthesised condition) and
364// lowered verbatim to C++ `if constexpr`, so the live branch is chosen at COMPILE time from
365// a constant condition. The constexpr-ness threads down the `else if constexpr` chain. A
366// cpp{} block supplies the constexpr fixtures so the program compiles AND runs; we also
367// assert the emitted C++ carries `if constexpr (` on both arms. Boolean logic (`and`,
368// comparisons) in the condition goes through the ordinary expression path.
369TEST(LangFeatures, IfConstexpr) {
370 const std::string gen = e2e::expect_e2e_source("lang_if_constexpr", R"PURR(import io
371cpp {
372constexpr int kMode = 2;
373constexpr bool kOn = true;
375fn 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 }
384io.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`";
396// ============================================================================
397// Compile-time `constexpr` family — `constexpr let` / `constexpr fn` /
398// `constexpr match`, plus the AUTO-promotion of `if`/`match` over a known
399// 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-time
404// constant, so a plain `if` over it AUTO-lowers to `if constexpr`.
405TEST(LangFeatures, ConstexprLetAutoIf) {
406 const std::string gen = e2e::expect_e2e_source("lang_cx_let_autoif", R"PURR(import io
407constexpr let N = 4
408if (N == 4) {
409 io.print("four")
410} else {
411 io.print("other")
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`";
420// Constant-folding initializer + reference to an earlier constexpr let (chained constants).
421TEST(LangFeatures, ConstexprLetArithmeticAndChain) {
422 const std::string gen = e2e::expect_e2e_source("lang_cx_let_chain", R"PURR(import io
423constexpr let A = 2 * 3 + 1
424constexpr let B = A + 5
425if (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);
433// Explicit type annotation on a constexpr let still emits `constexpr`.
434TEST(LangFeatures, ConstexprLetTyped) {
435 const std::string gen = e2e::expect_e2e_source("lang_cx_let_typed", R"PURR(import io
436constexpr let x: int = 5
437if (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);
444// Bool constexpr let drives an `if constexpr` with an `else` arm.
445TEST(LangFeatures, ConstexprLetBool) {
446 e2e::expect_e2e("lang_cx_let_bool", R"PURR(import io
447constexpr let on = true
448if (on) { io.print("on") } else { io.print("off") }
449)PURR",
450 "on\n");
453// A purely-literal condition is itself a constant -> auto `if constexpr`, no `let` needed.
454TEST(LangFeatures, AutoIfLiteralCondition) {
455 const std::string gen = e2e::expect_e2e_source("lang_cx_literal_if", R"PURR(import io
456if (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);
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).
464TEST(LangFeatures, RuntimeIfNotPromoted) {
465 const std::string gen = e2e::expect_e2e_source("lang_cx_runtime_if", R"PURR(import io
466fn label(x: int) {
467 if (x == 1) { return "one" } else { return "many" }
469io.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`";
476// `constexpr fn`: a constexpr function whose call folds inside a `constexpr let`, and which
477// is ALSO callable at runtime.
478TEST(LangFeatures, ConstexprFn) {
479 const std::string gen = e2e::expect_e2e_source("lang_cx_fn", R"PURR(import io
480constexpr fn square(x) { return x * x }
481constexpr let r = square(5)
482if (r == 25) { io.print("r25") } else { io.print("no") }
483io.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";
492// Explicit `constexpr match` over a constant subject -> compile-time `if constexpr` chain.
493TEST(LangFeatures, ConstexprMatchExplicit) {
494 const std::string gen = e2e::expect_e2e_source("lang_cx_match_explicit", R"PURR(import io
495constexpr let k = 2
496constexpr match k {
497 case 1 { io.print("one") }
498 case 2 { io.print("two") }
499 case _ { io.print("other") }
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";
509// AUTO: a plain `match` over a constant subject with constant case labels also folds to the
510// compile-time `if constexpr` chain (no `constexpr` keyword on the match needed).
511TEST(LangFeatures, MatchAutoConstexpr) {
512 const std::string gen = e2e::expect_e2e_source("lang_cx_match_auto", R"PURR(import io
513constexpr let k = 3
514match k {
515 case 1 { io.print("a") }
516 case 3 { io.print("c") }
517 case _ { io.print("z") }
519)PURR",
520 "c\n");
521 EXPECT_NE(gen.find("if constexpr ("), std::string::npos);
524// A plain `match` on a RUNTIME integer lowers to a real C++ `switch` (default = `_`).
525TEST(LangFeatures, MatchRuntimeIntSwitch) {
526 const std::string gen = e2e::expect_e2e_source("lang_match_switch", R"PURR(import io
527fn classify(n: int) {
528 match n {
529 case 1 { return "a" }
530 case 2 { return "b" }
531 case _ { return "c" }
532 }
534io.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);
542// GUARD: a `match` on a STRING cannot be a switch (C++ forbids it) — it must fall back to the
543// `==` if/else-if chain. The most important correctness guard of the smart lowering.
544TEST(LangFeatures, MatchStringFallsBackToChain) {
545 const std::string gen = e2e::expect_e2e_source("lang_match_string", R"PURR(import io
546fn code(s) {
547 match s {
548 case "red" { return 1 }
549 case "green" { return 2 }
550 case _ { return 0 }
551 }
553io.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)";
560// GUARD: bool case labels are not switch labels here either -> `==` chain.
561TEST(LangFeatures, MatchBoolFallsBackToChain) {
562 const std::string gen = e2e::expect_e2e_source("lang_match_bool", R"PURR(import io
563fn name(b) {
564 match b {
565 case true { return "yes" }
566 case false { return "no" }
567 }
569io.print(name(true), name(false))
570)PURR",
571 "yes no\n");
572 EXPECT_EQ(gen.find("switch ("), std::string::npos);
575// A switch case whose body RETURNS must not emit an unreachable trailing `break` (else
576// -Wunreachable-code/-Werror could reject it). Verifies it compiles and runs.
577TEST(LangFeatures, MatchSwitchCaseReturnsNoUnreachableBreak) {
578 e2e::expect_e2e("lang_match_return", R"PURR(import io
579fn pick(n: int) {
580 match n {
581 case 1 { return 10 }
582 case 2 { return 20 }
583 case _ { return 0 }
584 }
586io.print(pick(1), pick(2), pick(5))
587)PURR",
588 "10 20 0\n");
591// Negative integer case labels are valid switch labels (`case (-1):`).
592TEST(LangFeatures, MatchNegativeIntSwitch) {
593 const std::string gen = e2e::expect_e2e_source("lang_match_negative", R"PURR(import io
594fn sign(n: int) {
595 match n {
596 case -1 { return "neg" }
597 case 0 { return "zero" }
598 case _ { return "pos" }
599 }
601io.print(sign(-1), sign(0), sign(7))
602)PURR",
603 "neg zero pos\n");
604 EXPECT_NE(gen.find("switch ("), std::string::npos);
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 these
611// stay valid source. String-literal contexts (a struct's printed field label) keep the
612// ORIGINAL spelling, not the escaped one.
613TEST(LangFeatures, CppKeywordIdentifiersAreEscaped) {
614 const std::string gen = e2e::expect_e2e_source("lang_cpp_keyword_escape", R"PURR(import io
615struct Box {
616 new: int
617 default: str
618 fn delete(self) { return self.new }
620fn make(new) {
621 return Box({.new = new, .default = "d"})
623let b = make(5)
624io.print(b.delete())
625io.print(b.default)
626let switch = 3
627for template in range(0, switch) { io.print(template) }
628try { 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, and
632 // 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;
643// A program function may call one defined AFTER it, passing a program struct: the call
644// resolves by ADL on the argument type at instantiation. The emitted functions must
645// therefore live in the same namespace as the program's types — an anonymous namespace
646// around them (tried once for misc-use-anonymous-namespace) made this stop compiling in
647// a downstream app, and nothing in the corpus had exercised it.
648TEST(LangFeatures, ForwardReferenceThroughStructArgument) {
649 e2e::expect_e2e("lang_forward_reference", R"PURR(import io
650struct Token {
651 value: int
653fn render(t: Token) {
654 return describe(t)
656fn describe(t: Token) {
657 return "token " + str(t.value)
659io.print(render(Token({.value = 7})))
660)PURR",
661 "token 7\n");