-
Notifications
You must be signed in to change notification settings - Fork 15.4k
[Clang] [C++26] Expansion Statements (Part 7: Constexpr support and tests) #169686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: users/Sirraide/expansion-stmts-6-destructuring
Are you sure you want to change the base?
[Clang] [C++26] Expansion Statements (Part 7: Constexpr support and tests) #169686
Conversation
🐧 Linux x64 Test Results
|
|
@llvm/pr-subscribers-clang @llvm/pr-subscribers-clang-codegen Author: None (Sirraide) ChangesPatch is 35.53 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/169686.diff 4 Files Affected:
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index d93f87a27e68d..2de1641f6b46a 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -6037,6 +6037,12 @@ static EvalStmtResult EvaluateStmt(StmtResult &Result, EvalInfo &Info,
const VarDecl *VD = dyn_cast_or_null<VarDecl>(D);
if (VD && !CheckLocalVariableDeclaration(Info, VD))
return ESR_Failed;
+
+ if (const auto *ESD = dyn_cast<CXXExpansionStmtDecl>(D)) {
+ assert(ESD->getInstantiations() && "not expanded?");
+ return EvaluateStmt(Result, Info, ESD->getInstantiations(), Case);
+ }
+
// Each declaration initialization is its own full-expression.
FullExpressionRAII Scope(Info);
if (!EvaluateDecl(Info, D, /*EvaluateConditionDecl=*/true) &&
@@ -6309,6 +6315,40 @@ static EvalStmtResult EvaluateStmt(StmtResult &Result, EvalInfo &Info,
return Scope.destroy() ? ESR_Succeeded : ESR_Failed;
}
+ case Stmt::CXXExpansionStmtInstantiationClass: {
+ BlockScopeRAII Scope(Info);
+ const auto *Expansion = cast<CXXExpansionStmtInstantiation>(S);
+ for (const Stmt *Shared : Expansion->getSharedStmts()) {
+ EvalStmtResult ESR = EvaluateStmt(Result, Info, Shared);
+ if (ESR != ESR_Succeeded) {
+ if (ESR != ESR_Failed && !Scope.destroy())
+ return ESR_Failed;
+ return ESR;
+ }
+ }
+
+ // No need to push an extra scope for these since they're already
+ // CompoundStmts.
+ EvalStmtResult ESR = ESR_Succeeded;
+ for (const Stmt *Instantiation : Expansion->getInstantiations()) {
+ ESR = EvaluateStmt(Result, Info, Instantiation);
+ if (ESR == ESR_Failed ||
+ ShouldPropagateBreakContinue(Info, Expansion, &Scope, ESR))
+ return ESR;
+ if (ESR != ESR_Continue) {
+ // Succeeded here actually means we encountered a 'break'.
+ assert(ESR == ESR_Succeeded || ESR == ESR_Returned);
+ break;
+ }
+ }
+
+ // Map Continue back to Succeeded if we fell off the end of the loop.
+ if (ESR == ESR_Continue)
+ ESR = ESR_Succeeded;
+
+ return Scope.destroy() ? ESR : ESR_Failed;
+ }
+
case Stmt::SwitchStmtClass:
return EvaluateSwitch(Result, Info, cast<SwitchStmt>(S));
diff --git a/clang/lib/CodeGen/CGDecl.cpp b/clang/lib/CodeGen/CGDecl.cpp
index 8b1cd83af2396..678d2e9fa743a 100644
--- a/clang/lib/CodeGen/CGDecl.cpp
+++ b/clang/lib/CodeGen/CGDecl.cpp
@@ -143,6 +143,13 @@ void CodeGenFunction::EmitDecl(const Decl &D, bool EvaluateConditionDecl) {
// None of these decls require codegen support.
return;
+ case Decl::CXXExpansionStmt: {
+ const auto *ESD = cast<CXXExpansionStmtDecl>(&D);
+ assert(ESD->getInstantiations() && "expansion statement not expanded?");
+ EmitStmt(ESD->getInstantiations());
+ return;
+ }
+
case Decl::NamespaceAlias:
if (CGDebugInfo *DI = getDebugInfo())
DI->EmitNamespaceAlias(cast<NamespaceAliasDecl>(D));
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index 8030aac3d8771..e398710ace63b 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -2027,6 +2027,9 @@ static bool CheckConstexprDeclStmt(Sema &SemaRef, const FunctionDecl *Dcl,
// - using-enum-declaration
continue;
+ case Decl::CXXExpansionStmt:
+ continue;
+
case Decl::Typedef:
case Decl::TypeAlias: {
// - typedef declarations and alias-declarations that do not define
diff --git a/clang/test/SemaCXX/cxx2c-expansion-stmts.cpp b/clang/test/SemaCXX/cxx2c-expansion-stmts.cpp
new file mode 100644
index 0000000000000..71ce71c4f69fe
--- /dev/null
+++ b/clang/test/SemaCXX/cxx2c-expansion-stmts.cpp
@@ -0,0 +1,1042 @@
+// RUN: %clang_cc1 %s -std=c++2c -fsyntax-only -fdeclspec -fblocks -verify
+namespace std {
+template <typename T>
+struct initializer_list {
+ const T* a;
+ const T* b;
+ initializer_list(T* a, T* b): a{a}, b{b} {}
+};
+}
+
+struct S {
+ int x;
+ constexpr S(int x) : x{x} {}
+};
+
+void g(int); // #g
+template <int n> constexpr int tg() { return n; }
+
+void f1() {
+ template for (auto x : {}) static_assert(false, "discarded");
+ template for (constexpr auto x : {}) static_assert(false, "discarded");
+ template for (auto x : {1}) g(x);
+ template for (auto x : {1, 2, 3}) g(x);
+ template for (constexpr auto x : {1}) g(x);
+ template for (constexpr auto x : {1, 2, 3}) g(x);
+ template for (constexpr auto x : {1}) tg<x>();
+ template for (constexpr auto x : {1, 2, 3})
+ static_assert(tg<x>());
+
+ template for (int x : {1, 2, 3}) g(x);
+ template for (S x : {1, 2, 3}) g(x.x);
+ template for (constexpr S x : {1, 2, 3}) tg<x.x>();
+
+ template for (int x : {"1", S(1), {1, 2}}) { // expected-error {{cannot initialize a variable of type 'int' with an lvalue of type 'const char[2]'}} \
+ expected-error {{no viable conversion from 'S' to 'int'}} \
+ expected-error {{excess elements in scalar initializer}} \
+ expected-note 3 {{in instantiation of expansion statement requested here}}
+ g(x);
+ }
+
+ template for (constexpr auto x : {1, 2, 3, 4}) { // expected-note 3 {{in instantiation of expansion statement requested here}}
+ static_assert(tg<x>() == 4); // expected-error 3 {{static assertion failed due to requirement 'tg<x>() == 4'}} \
+ expected-note {{expression evaluates to '1 == 4'}} \
+ expected-note {{expression evaluates to '2 == 4'}} \
+ expected-note {{expression evaluates to '3 == 4'}}
+ }
+
+
+ template for (constexpr auto x : {1, 2}) { // expected-note 2 {{in instantiation of expansion statement requested here}}
+ static_assert(false, "not discarded"); // expected-error 2 {{static assertion failed: not discarded}}
+ }
+}
+
+template <typename T>
+void t1() {
+ template for (T x : {}) g(x);
+ template for (constexpr T x : {}) g(x);
+ template for (auto x : {}) g(x);
+ template for (constexpr auto x : {}) g(x);
+ template for (T x : {1, 2}) g(x);
+ template for (T x : {T(1), T(2)}) g(x);
+ template for (auto x : {T(1), T(2)}) g(x);
+ template for (constexpr T x : {T(1), T(2)}) static_assert(tg<x>());
+ template for (constexpr auto x : {T(1), T(2)}) static_assert(tg<x>());
+}
+
+template <typename U>
+struct s1 {
+ template <typename T>
+ void tf() {
+ template for (T x : {}) g(x);
+ template for (constexpr T x : {}) g(x);
+ template for (U x : {}) g(x);
+ template for (constexpr U x : {}) g(x);
+ template for (auto x : {}) g(x);
+ template for (constexpr auto x : {}) g(x);
+ template for (T x : {1, 2}) g(x);
+ template for (U x : {1, 2}) g(x);
+ template for (U x : {T(1), T(2)}) g(x);
+ template for (T x : {U(1), U(2)}) g(x);
+ template for (auto x : {T(1), T(2)}) g(x);
+ template for (auto x : {U(1), T(2)}) g(x);
+ template for (constexpr U x : {T(1), T(2)}) static_assert(tg<x>());
+ template for (constexpr T x : {U(1), U(2)}) static_assert(tg<x>());
+ template for (constexpr auto x : {T(1), U(2)}) static_assert(tg<x>());
+ }
+};
+
+template <typename T>
+void t2() {
+ template for (T x : {}) g(x);
+}
+
+void f2() {
+ t1<int>();
+ t1<long>();
+ s1<long>().tf<long>();
+ s1<int>().tf<int>();
+ s1<int>().tf<long>();
+ s1<long>().tf<int>();
+ t2<S>();
+ t2<S[1231]>();
+ t2<S***>();
+}
+
+template <__SIZE_TYPE__ size>
+struct String {
+ char data[size];
+
+ template <__SIZE_TYPE__ n>
+ constexpr String(const char (&str)[n]) { __builtin_memcpy(data, str, n); }
+
+ constexpr const char* begin() const { return data; }
+ constexpr const char* end() const { return data + size - 1; }
+};
+
+template <__SIZE_TYPE__ n>
+String(const char (&str)[n]) -> String<n>;
+
+constexpr int f3() {
+ static constexpr String s{"abcd"};
+ int count = 0;
+ template for (constexpr auto x : s) count++;
+ return count;
+}
+
+template <String s>
+constexpr int tf3() {
+ int count = 0;
+ template for (constexpr auto x : s) count++;
+ return count;
+}
+
+static_assert(f3() == 4);
+static_assert(tf3<"1">() == 1);
+static_assert(tf3<"12">() == 2);
+static_assert(tf3<"123">() == 3);
+static_assert(tf3<"1234">() == 4);
+
+void f4() {
+ static constexpr String empty{""};
+ static constexpr String s{"abcd"};
+ template for (auto x : empty) static_assert(false, "not expanded");
+ template for (constexpr auto x : s) g(x);
+ template for (auto x : s) g(x);
+}
+
+struct NegativeSize {
+ static constexpr const char* str = "123";
+ constexpr const char* begin() const { return str + 3; }
+ constexpr const char* end() const { return str; }
+};
+
+void negative_size() {
+ static constexpr NegativeSize n;
+ template for (auto x : n) g(x); // expected-error {{expansion size must not be negative (was -3)}}
+ template for (constexpr auto x : n) g(x); // expected-error {{expansion size must not be negative (was -3)}}
+}
+
+template <typename T, __SIZE_TYPE__ size>
+struct Array {
+ T data[size]{};
+ constexpr const T* begin() const { return data; }
+ constexpr const T* end() const { return data + size; }
+};
+
+struct NotInt {
+ struct iterator {};
+ constexpr iterator begin() const { return {}; }
+ constexpr iterator end() const { return {}; }
+};
+
+void not_int() {
+ static constexpr NotInt ni;
+ template for (auto x : ni) g(x); // expected-error {{invalid operands to binary expression}}
+}
+
+static constexpr Array<int, 3> integers{1, 2, 3};
+
+constexpr int friend_func();
+
+struct Private {
+ friend constexpr int friend_func();
+
+private:
+ constexpr const int* begin() const { return integers.begin(); } // expected-note 2 {{declared private here}}
+ constexpr const int* end() const { return integers.end(); } // expected-note 2 {{declared private here}}
+
+public:
+ static constexpr int member_func() {
+ int sum = 0;
+ static constexpr Private p1;
+ template for (auto x : p1) sum += x;
+ return sum;
+ }
+};
+
+struct Protected {
+ friend constexpr int friend_func();
+
+protected:
+ constexpr const int* begin() const { return integers.begin(); } // expected-note 2 {{declared protected here}}
+ constexpr const int* end() const { return integers.end(); } // expected-note 2 {{declared protected here}}
+
+public:
+ static constexpr int member_func() {
+ int sum = 0;
+ static constexpr Protected p1;
+ template for (auto x : p1) sum += x;
+ return sum;
+ }
+};
+
+void access_control() {
+ static constexpr Private p1;
+ template for (auto x : p1) g(x); // expected-error 2 {{'begin' is a private member of 'Private'}} expected-error 2 {{'end' is a private member of 'Private'}}
+
+ static constexpr Protected p2;
+ template for (auto x : p2) g(x); // expected-error 2 {{'begin' is a protected member of 'Protected'}} expected-error 2 {{'end' is a protected member of 'Protected'}}
+}
+
+constexpr int friend_func() {
+ int sum = 0;
+ static constexpr Private p1;
+ template for (auto x : p1) sum += x;
+
+ static constexpr Protected p2;
+ template for (auto x : p2) sum += x;
+ return sum;
+}
+
+static_assert(friend_func() == 12);
+static_assert(Private::member_func() == 6);
+static_assert(Protected::member_func() == 6);
+
+struct SizeNotICE {
+ struct iterator {
+ friend constexpr iterator operator+(iterator a, __PTRDIFF_TYPE__) { return a; }
+ int constexpr operator*() const { return 7; }
+
+ // NOT constexpr!
+ friend int operator-(iterator, iterator) { return 7; } // expected-note {{declared here}}
+ friend int operator!=(iterator, iterator) { return 7; }
+ };
+ constexpr iterator begin() const { return {}; }
+ constexpr iterator end() const { return {}; }
+};
+
+struct PlusMissing {
+ struct iterator {
+ int constexpr operator*() const { return 7; }
+ };
+ constexpr iterator begin() const { return {}; }
+ constexpr iterator end() const { return {}; }
+};
+
+struct DerefMissing {
+ struct iterator {
+ friend constexpr iterator operator+(iterator a, __PTRDIFF_TYPE__) { return a; }
+ };
+ constexpr iterator begin() const { return {}; }
+ constexpr iterator end() const { return {}; }
+};
+
+void missing_funcs() {
+ static constexpr SizeNotICE s1;
+ static constexpr PlusMissing s2;
+ static constexpr DerefMissing s3;
+
+ // TODO: This message should start complaining about '!=' once we support the
+ // proper way of computing the size.
+ template for (auto x : s1) g(x); // expected-error {{expansion size is not a constant expression}} \
+ expected-note {{non-constexpr function 'operator-' cannot be used in a constant expression}}
+
+ template for (auto x : s2) g(x); // expected-error {{invalid operands to binary expression}}
+ template for (auto x : s3) g(x); // expected-error {{indirection requires pointer operand ('iterator' invalid)}}
+}
+
+namespace adl {
+struct ADL {
+
+};
+
+constexpr const int* begin(const ADL&) { return integers.begin(); }
+constexpr const int* end(const ADL&) { return integers.end(); }
+}
+
+namespace adl_error {
+struct ADLError1 {
+ constexpr const int* begin() const { return integers.begin(); }
+};
+
+struct ADLError2 {
+ constexpr const int* end() const { return integers.end(); }
+};
+
+constexpr const int* begin(const ADLError2&) { return integers.begin(); }
+constexpr const int* end(const ADLError1&) { return integers.end(); }
+}
+
+namespace adl_both {
+static constexpr Array<int, 5> integers2{1, 2, 3, 4, 5};
+struct ADLBoth {
+ // Test that member begin/end are preferred over ADl begin/end. These return
+ // pointers to a different array.
+ constexpr const int* begin() const { return integers2.begin(); }
+ constexpr const int* end() const { return integers2.end(); }
+};
+
+constexpr const int* begin(const ADLBoth&) { return integers.begin(); }
+constexpr const int* end(const ADLBoth&) { return integers.end(); }
+}
+
+constexpr int adl_begin_end() {
+ static constexpr adl::ADL a;
+ int sum = 0;
+ template for (auto x : a) sum += x;
+ template for (constexpr auto x : a) sum += x;
+ return sum;
+}
+
+static_assert(adl_begin_end() == 12);
+
+void adl_mixed() {
+ static constexpr adl_error::ADLError1 a1;
+ static constexpr adl_error::ADLError2 a2;
+
+ // These are actually destructuring because there is no
+ // valid begin/end pair.
+ template for (auto x : a1) g(x);
+ template for (auto x : a2) g(x);
+}
+
+constexpr int adl_both_test() {
+ static constexpr adl_both::ADLBoth a;
+ int sum = 0;
+ template for (auto x : a) sum += x;
+ return sum;
+}
+
+static_assert(adl_both_test() == 15);
+
+struct A {};
+struct B { int x = 1; };
+struct C { int a = 1, b = 2, c = 3; };
+struct D {
+ int a = 1;
+ int* b = nullptr;
+ const char* c = "3";
+};
+
+struct Nested {
+ A a;
+ B b;
+ C c;
+};
+
+struct PrivateDestructurable {
+ friend void destructurable_friend();
+private:
+ int a, b; // expected-note 4 {{declared private here}}
+};
+
+struct ProtectedDestructurable {
+ friend void destructurable_friend();
+protected:
+ int a, b; // expected-note 4 {{declared protected here}}
+};
+
+void destructuring() {
+ static constexpr A a;
+ static constexpr B b;
+ static constexpr C c;
+ static constexpr D d;
+
+ template for (auto x : a) static_assert(false, "not expanded");
+ template for (constexpr auto x : a) static_assert(false, "not expanded");
+
+ template for (auto x : b) g(x);
+ template for (constexpr auto x : b) g(x);
+
+ template for (auto x : c) g(x);
+ template for (constexpr auto x : c) g(x);
+
+ template for (auto x : d) { // expected-note 2 {{in instantiation of expansion statement requested here}}
+ // expected-note@#g {{candidate function not viable: no known conversion from 'int *' to 'int' for 1st argument}}
+ // expected-note@#g {{candidate function not viable: no known conversion from 'const char *' to 'int' for 1st argument}}
+ g(x); // expected-error 2 {{no matching function for call to 'g'}}
+
+ }
+
+ template for (constexpr auto x : d) { // expected-note 2 {{in instantiation of expansion statement requested here}}
+ // expected-note@#g {{candidate function not viable: no known conversion from 'int *const' to 'int' for 1st argument}}
+ // expected-note@#g {{candidate function not viable: no known conversion from 'const char *const' to 'int' for 1st argument}}
+ g(x); // expected-error 2 {{no matching function for call to 'g'}}
+ }
+}
+
+constexpr int array() {
+ static constexpr int x[4]{1, 2, 3, 4};
+ int sum = 0;
+ template for (auto y : x) sum += y;
+ template for (constexpr auto y : x) sum += y;
+ return sum;
+}
+
+static_assert(array() == 20);
+
+template <auto v>
+constexpr int destructure() {
+ int sum = 0;
+ template for (auto x : v) sum += x;
+ template for (constexpr auto x : v) sum += x;
+ return sum;
+}
+
+static_assert(destructure<B{10}>() == 20);
+static_assert(destructure<C{}>() == 12);
+static_assert(destructure<C{3, 4, 5}>() == 24);
+
+constexpr int nested() {
+ static constexpr Nested n;
+ int sum = 0;
+ template for (constexpr auto x : n) {
+ static constexpr auto val = x;
+ template for (auto y : val) {
+ sum += y;
+ }
+ }
+ template for (constexpr auto x : n) {
+ static constexpr auto val = x;
+ template for (constexpr auto y : val) {
+ sum += y;
+ }
+ }
+ return sum;
+}
+
+static_assert(nested() == 14);
+
+void access_control_destructurable() {
+ template for (auto x : PrivateDestructurable()) {} // expected-error 2 {{cannot bind private member 'a' of 'PrivateDestructurable'}} \
+ expected-error 2 {{cannot bind private member 'b' of 'PrivateDestructurable'}}
+
+ template for (auto x : ProtectedDestructurable()) {} // expected-error 2 {{cannot bind protected member 'a' of 'ProtectedDestructurable'}} \
+ expected-error 2 {{cannot bind protected member 'b' of 'ProtectedDestructurable'}}
+}
+
+void destructurable_friend() {
+ template for (auto x : PrivateDestructurable()) {}
+ template for (auto x : ProtectedDestructurable()) {}
+}
+
+struct Placeholder {
+ A get_value() const { return {}; }
+ __declspec(property(get = get_value)) A a;
+};
+
+void placeholder() {
+ template for (auto x: Placeholder().a) {}
+}
+
+union Union { int a; long b;};
+
+struct MemberPtr {
+ void f() {}
+};
+
+void overload_set(int); // expected-note 2 {{possible target for call}}
+void overload_set(long); // expected-note 2 {{possible target for call}}
+
+void invalid_types() {
+ template for (auto x : void()) {} // expected-error {{cannot expand expression of type 'void'}}
+ template for (auto x : 1) {} // expected-error {{cannot expand expression of type 'int'}}
+ template for (auto x : 1.f) {} // expected-error {{cannot expand expression of type 'float'}}
+ template for (auto x : 'c') {} // expected-error {{cannot expand expression of type 'char'}}
+ template for (auto x : invalid_types) {} // expected-error {{cannot expand expression of type 'void ()'}}
+ template for (auto x : &invalid_types) {} // expected-error {{cannot expand expression of type 'void (*)()'}}
+ template for (auto x : &MemberPtr::f) {} // expected-error {{cannot expand expression of type 'void (MemberPtr::*)()'}}
+ template for (auto x : overload_set) {} // expected-error{{reference to overloaded function could not be resolved; did you mean to call it?}}
+ template for (auto x : &overload_set) {} // expected-error{{reference to overloaded function could not be resolved; did you mean to call it?}}
+ template for (auto x : nullptr) {} // expected-error {{cannot expand expression of type 'std::nullptr_t'}}
+ template for (auto x : __builtin_strlen) {} // expected-error {{builtin functions must be directly called}}
+ template for (auto x : Union()) {} // expected-error {{cannot expand expression of type 'Union'}}
+ template for (auto x : (char*)nullptr) {} // expected-error {{cannot expand expression of type 'char *'}}
+ template for (auto x : []{}) {} // expected-error {{cannot expand lambda closure type}}
+ template for (auto x : [x=3]{}) {} // expected-error {{cannot expand lambda closure type}}
+}
+
+struct BeginOnly {
+ int x{1};
+ constexpr const int* begin() const { r...
[truncated]
|
d3761e5 to
f1a507f
Compare
43c4a23 to
45151d1
Compare
f1a507f to
bb0804e
Compare
45151d1 to
b874722
Compare
| template <int n> constexpr int tg() { return n; } | ||
|
|
||
| void f1() { | ||
| template for (auto x : {}) static_assert(false, "discarded"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should add a test that combines init-statment with an empty range. The expectation is that init-statement should be executed anyway (https://godbolt.org/z/aKvKzMq5e)
I also would like to see a test where the range is VLA, which hopefully produces an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also forgot to check for incomplete types, which currently crashes; I’ll fix that and add a test for that as well.
bb0804e to
6fcfc03
Compare
b874722 to
2db2d6d
Compare
6fcfc03 to
dd84520
Compare
2db2d6d to
13fb7c8
Compare
|
Can you clarify the status of the following Core issues in your implementation: I'd also be very grateful if you added tests for those Core issues. Probably not right now, as the last two hasn't even been discussed in Core, and because we can't run |
|
They’re essentially all already implemented:
Then should I make that a follow-up patch after this entire patch series is merged? |
|
There is also CWG 3043, which is also implemented. |
I think it's worth being more careful with the wording here, because CWG3123 and CWG3131 have not been discussed by Core yet, so we don't know whether it agrees with the suggested resolutions. |
dd84520 to
6195321
Compare
13fb7c8 to
07e223d
Compare

This implements compile-time evaluation of expansion statements. With this implemented, we can finally test expansion statements properly, so this also adds a bunch of tests for all three kinds of expansion statements.
I didn’t add any Sema tests before this because I felt that just testing that we don’t emit any diagnostics would not have been too useful; with this patch, we can also test that the behaviour of the generated AST is correct by evaluating it.
I still have to add support to the new constant interpreter; I’ll be adding that to this patch as well.