| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| //===-- CachedConstAccessorsLattice.h ---------------------------*- C++ -*-===// | ||
| // | ||
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| // See https://llvm.org/LICENSE.txt for license information. | ||
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
| // | ||
| // This file defines the lattice mixin that additionally maintains a cache of | ||
| // stable method call return values to model const accessor member functions. | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| #ifndef LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_CACHED_CONST_ACCESSORS_LATTICE_H | ||
| #define LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_CACHED_CONST_ACCESSORS_LATTICE_H | ||
|
|
||
| #include "clang/AST/Expr.h" | ||
| #include "clang/Analysis/FlowSensitive/DataflowEnvironment.h" | ||
| #include "clang/Analysis/FlowSensitive/DataflowLattice.h" | ||
| #include "clang/Analysis/FlowSensitive/StorageLocation.h" | ||
| #include "clang/Analysis/FlowSensitive/Value.h" | ||
| #include "llvm/ADT/DenseMap.h" | ||
| #include "llvm/ADT/STLFunctionalExtras.h" | ||
|
|
||
| namespace clang { | ||
| namespace dataflow { | ||
|
|
||
| /// A mixin for a lattice that additionally maintains a cache of stable method | ||
| /// call return values to model const accessors methods. When a non-const method | ||
| /// is called, the cache should be cleared causing the next call to a const | ||
| /// method to be considered a different value. NOTE: The user is responsible for | ||
| /// clearing the cache. | ||
| /// | ||
| /// For example: | ||
| /// | ||
| /// class Bar { | ||
| /// public: | ||
| /// const std::optional<Foo>& getFoo() const; | ||
| /// void clear(); | ||
| /// }; | ||
| // | ||
| /// void func(Bar& s) { | ||
| /// if (s.getFoo().has_value()) { | ||
| /// use(s.getFoo().value()); // safe (checked earlier getFoo()) | ||
| /// s.clear(); | ||
| /// use(s.getFoo().value()); // unsafe (invalidate cache for s) | ||
| /// } | ||
| /// } | ||
| template <typename Base> class CachedConstAccessorsLattice : public Base { | ||
| public: | ||
| using Base::Base; // inherit all constructors | ||
|
|
||
| /// Creates or returns a previously created `Value` associated with a const | ||
| /// method call `obj.getFoo()` where `RecordLoc` is the | ||
| /// `RecordStorageLocation` of `obj`. | ||
| /// Returns nullptr if unable to find or create a value. | ||
| /// | ||
| /// Requirements: | ||
| /// | ||
| /// - `CE` should return a value (not a reference or record type) | ||
| Value * | ||
| getOrCreateConstMethodReturnValue(const RecordStorageLocation &RecordLoc, | ||
| const CallExpr *CE, Environment &Env); | ||
|
|
||
| /// Creates or returns a previously created `StorageLocation` associated with | ||
| /// a const method call `obj.getFoo()` where `RecordLoc` is the | ||
| /// `RecordStorageLocation` of `obj`. | ||
| /// | ||
| /// The callback `Initialize` runs on the storage location if newly created. | ||
| /// Returns nullptr if unable to find or create a value. | ||
| /// | ||
| /// Requirements: | ||
| /// | ||
| /// - `CE` should return a location (GLValue or a record type). | ||
| StorageLocation *getOrCreateConstMethodReturnStorageLocation( | ||
| const RecordStorageLocation &RecordLoc, const CallExpr *CE, | ||
| Environment &Env, llvm::function_ref<void(StorageLocation &)> Initialize); | ||
|
|
||
| void clearConstMethodReturnValues(const RecordStorageLocation &RecordLoc) { | ||
| ConstMethodReturnValues.erase(&RecordLoc); | ||
| } | ||
|
|
||
| void clearConstMethodReturnStorageLocations( | ||
| const RecordStorageLocation &RecordLoc) { | ||
| ConstMethodReturnStorageLocations.erase(&RecordLoc); | ||
| } | ||
|
|
||
| bool operator==(const CachedConstAccessorsLattice &Other) const { | ||
| return Base::operator==(Other); | ||
| } | ||
|
|
||
| LatticeJoinEffect join(const CachedConstAccessorsLattice &Other); | ||
|
|
||
| private: | ||
| // Maps a record storage location and const method to the value to return | ||
| // from that const method. | ||
| using ConstMethodReturnValuesType = | ||
| llvm::SmallDenseMap<const RecordStorageLocation *, | ||
| llvm::SmallDenseMap<const FunctionDecl *, Value *>>; | ||
| ConstMethodReturnValuesType ConstMethodReturnValues; | ||
|
|
||
| // Maps a record storage location and const method to the record storage | ||
| // location to return from that const method. | ||
| using ConstMethodReturnStorageLocationsType = llvm::SmallDenseMap< | ||
| const RecordStorageLocation *, | ||
| llvm::SmallDenseMap<const FunctionDecl *, StorageLocation *>>; | ||
| ConstMethodReturnStorageLocationsType ConstMethodReturnStorageLocations; | ||
| }; | ||
|
|
||
| namespace internal { | ||
|
|
||
| template <typename T> | ||
| llvm::SmallDenseMap<const RecordStorageLocation *, | ||
| llvm::SmallDenseMap<const FunctionDecl *, T *>> | ||
| joinConstMethodMap( | ||
| const llvm::SmallDenseMap<const RecordStorageLocation *, | ||
| llvm::SmallDenseMap<const FunctionDecl *, T *>> | ||
| &Map1, | ||
| const llvm::SmallDenseMap<const RecordStorageLocation *, | ||
| llvm::SmallDenseMap<const FunctionDecl *, T *>> | ||
| &Map2, | ||
| LatticeEffect &Effect) { | ||
| llvm::SmallDenseMap<const RecordStorageLocation *, | ||
| llvm::SmallDenseMap<const FunctionDecl *, T *>> | ||
| Result; | ||
| for (auto &[Loc, DeclToT] : Map1) { | ||
| auto It = Map2.find(Loc); | ||
| if (It == Map2.end()) { | ||
| Effect = LatticeJoinEffect::Changed; | ||
| continue; | ||
| } | ||
| const auto &OtherDeclToT = It->second; | ||
| auto &JoinedDeclToT = Result[Loc]; | ||
| for (auto [Func, Var] : DeclToT) { | ||
| T *OtherVar = OtherDeclToT.lookup(Func); | ||
| if (OtherVar == nullptr || OtherVar != Var) { | ||
| Effect = LatticeJoinEffect::Changed; | ||
| continue; | ||
| } | ||
| JoinedDeclToT.insert({Func, Var}); | ||
| } | ||
| } | ||
| return Result; | ||
| } | ||
|
|
||
| } // namespace internal | ||
|
|
||
| template <typename Base> | ||
| LatticeEffect CachedConstAccessorsLattice<Base>::join( | ||
| const CachedConstAccessorsLattice<Base> &Other) { | ||
|
|
||
| LatticeEffect Effect = Base::join(Other); | ||
|
|
||
| // For simplicity, we only retain values that are identical, but not ones that | ||
| // are non-identical but equivalent. This is likely to be sufficient in | ||
| // practice, and it reduces implementation complexity considerably. | ||
|
|
||
| ConstMethodReturnValues = internal::joinConstMethodMap<Value>( | ||
| ConstMethodReturnValues, Other.ConstMethodReturnValues, Effect); | ||
|
|
||
| ConstMethodReturnStorageLocations = | ||
| internal::joinConstMethodMap<StorageLocation>( | ||
| ConstMethodReturnStorageLocations, | ||
| Other.ConstMethodReturnStorageLocations, Effect); | ||
|
|
||
| return Effect; | ||
| } | ||
|
|
||
| template <typename Base> | ||
| Value *CachedConstAccessorsLattice<Base>::getOrCreateConstMethodReturnValue( | ||
| const RecordStorageLocation &RecordLoc, const CallExpr *CE, | ||
| Environment &Env) { | ||
| QualType Type = CE->getType(); | ||
| assert(!Type.isNull()); | ||
| assert(!Type->isReferenceType()); | ||
| assert(!Type->isRecordType()); | ||
|
|
||
| auto &ObjMap = ConstMethodReturnValues[&RecordLoc]; | ||
| const FunctionDecl *DirectCallee = CE->getDirectCallee(); | ||
| if (DirectCallee == nullptr) | ||
| return nullptr; | ||
| auto it = ObjMap.find(DirectCallee); | ||
| if (it != ObjMap.end()) | ||
| return it->second; | ||
|
|
||
| Value *Val = Env.createValue(Type); | ||
| if (Val != nullptr) | ||
| ObjMap.insert({DirectCallee, Val}); | ||
| return Val; | ||
| } | ||
|
|
||
| template <typename Base> | ||
| StorageLocation * | ||
| CachedConstAccessorsLattice<Base>::getOrCreateConstMethodReturnStorageLocation( | ||
| const RecordStorageLocation &RecordLoc, const CallExpr *CE, | ||
| Environment &Env, llvm::function_ref<void(StorageLocation &)> Initialize) { | ||
| assert(!CE->getType().isNull()); | ||
| assert(CE->isGLValue() || CE->getType()->isRecordType()); | ||
| auto &ObjMap = ConstMethodReturnStorageLocations[&RecordLoc]; | ||
| const FunctionDecl *DirectCallee = CE->getDirectCallee(); | ||
| if (DirectCallee == nullptr) | ||
| return nullptr; | ||
| auto it = ObjMap.find(DirectCallee); | ||
| if (it != ObjMap.end()) | ||
| return it->second; | ||
|
|
||
| StorageLocation &Loc = | ||
| Env.createStorageLocation(CE->getType().getNonReferenceType()); | ||
| Initialize(Loc); | ||
|
|
||
| ObjMap.insert({DirectCallee, &Loc}); | ||
| return &Loc; | ||
| } | ||
|
|
||
| } // namespace dataflow | ||
| } // namespace clang | ||
|
|
||
| #endif // LLVM_CLANG_ANALYSIS_FLOWSENSITIVE_CACHED_CONST_ACCESSORS_LATTICE_H |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,175 +1,219 @@ | ||
| // RUN: %clang_cc1 -std=c++20 -verify %s | ||
| // expected-no-diagnostics | ||
|
|
||
| namespace Primary { | ||
| template<typename T> | ||
| concept D = true; | ||
|
|
||
| template<typename T> | ||
| struct A { | ||
| template<typename U, bool V> | ||
| void f() requires V; | ||
|
|
||
| template<> | ||
| void f<short, true>(); | ||
|
|
||
| template<D U> | ||
| void g(); | ||
|
|
||
| template<typename U, bool V> requires V | ||
| struct B; | ||
|
|
||
| template<typename U, bool V> requires V | ||
| struct B<U*, V>; | ||
|
|
||
| template<> | ||
| struct B<short, true>; | ||
|
|
||
| template<D U> | ||
| struct C; | ||
|
|
||
| template<D U> | ||
| struct C<U*>; | ||
|
|
||
| template<typename U, bool V> requires V | ||
| static int x; | ||
|
|
||
| template<typename U, bool V> requires V | ||
| static int x<U*, V>; | ||
|
|
||
| template<> | ||
| int x<short, true>; | ||
|
|
||
| template<D U> | ||
| static int y; | ||
|
|
||
| template<D U> | ||
| static int y<U*>; | ||
| }; | ||
|
|
||
| template<typename T> | ||
| template<typename U, bool V> | ||
| void A<T>::f() requires V { } | ||
|
|
||
| template<typename T> | ||
| template<D U> | ||
| void A<T>::g() { } | ||
|
|
||
| template<typename T> | ||
| template<typename U, bool V> requires V | ||
| struct A<T>::B { }; | ||
|
|
||
| template<typename T> | ||
| template<typename U, bool V> requires V | ||
| struct A<T>::B<U*, V> { }; | ||
|
|
||
| template<typename T> | ||
| template<typename U, bool V> requires V | ||
| struct A<T>::B<U&, V> { }; | ||
|
|
||
| template<typename T> | ||
| template<D U> | ||
| struct A<T>::C { }; | ||
|
|
||
| template<typename T> | ||
| template<D U> | ||
| struct A<T>::C<U*> { }; | ||
|
|
||
| template<typename T> | ||
| template<typename U, bool V> requires V | ||
| int A<T>::x = 0; | ||
|
|
||
| template<typename T> | ||
| template<typename U, bool V> requires V | ||
| int A<T>::x<U*, V> = 0; | ||
|
|
||
| template<typename T> | ||
| template<typename U, bool V> requires V | ||
| int A<T>::x<U&, V> = 0; | ||
|
|
||
| template<typename T> | ||
| template<D U> | ||
| int A<T>::y = 0; | ||
|
|
||
| template<typename T> | ||
| template<D U> | ||
| int A<T>::y<U*> = 0; | ||
|
|
||
| template<> | ||
| template<typename U, bool V> | ||
| void A<short>::f() requires V; | ||
|
|
||
| template<> | ||
| template<> | ||
| void A<short>::f<int, true>(); | ||
|
|
||
| template<> | ||
| template<> | ||
| void A<void>::f<int, true>(); | ||
|
|
||
| template<> | ||
| template<D U> | ||
| void A<short>::g(); | ||
|
|
||
| template<> | ||
| template<typename U, bool V> requires V | ||
| struct A<int>::B; | ||
|
|
||
| template<> | ||
| template<> | ||
| struct A<int>::B<int, true>; | ||
|
|
||
| template<> | ||
| template<> | ||
| struct A<void>::B<int, true>; | ||
|
|
||
| template<> | ||
| template<typename U, bool V> requires V | ||
| struct A<int>::B<U*, V>; | ||
|
|
||
| template<> | ||
| template<typename U, bool V> requires V | ||
| struct A<int>::B<U&, V>; | ||
|
|
||
| template<> | ||
| template<D U> | ||
| struct A<int>::C; | ||
|
|
||
| template<> | ||
| template<D U> | ||
| struct A<int>::C<U*>; | ||
|
|
||
| template<> | ||
| template<D U> | ||
| struct A<int>::C<U&>; | ||
|
|
||
| template<> | ||
| template<typename U, bool V> requires V | ||
| int A<long>::x; | ||
|
|
||
| template<> | ||
| template<> | ||
| int A<long>::x<int, true>; | ||
|
|
||
| template<> | ||
| template<> | ||
| int A<void>::x<int, true>; | ||
|
|
||
| template<> | ||
| template<typename U, bool V> requires V | ||
| int A<long>::x<U*, V>; | ||
|
|
||
| template<> | ||
| template<typename U, bool V> requires V | ||
| int A<long>::x<U&, V>; | ||
|
|
||
| template<> | ||
| template<D U> | ||
| int A<long>::y; | ||
|
|
||
| template<> | ||
| template<D U> | ||
| int A<long>::y<U*>; | ||
|
|
||
| template<> | ||
| template<D U> | ||
| int A<long>::y<U&>; | ||
| } // namespace Primary | ||
|
|
||
| namespace Partial { | ||
| template<typename T, bool B> | ||
| struct A; | ||
|
|
||
| template<bool U> | ||
| struct A<int, U> | ||
| { | ||
| template<typename V> requires U | ||
| void f(); | ||
|
|
||
| template<typename V> requires U | ||
| static const int x; | ||
|
|
||
| template<typename V> requires U | ||
| struct B; | ||
| }; | ||
|
|
||
| template<bool U> | ||
| template<typename V> requires U | ||
| void A<int, U>::f() { } | ||
|
|
||
| template<bool U> | ||
| template<typename V> requires U | ||
| constexpr int A<int, U>::x = 0; | ||
|
|
||
| template<bool U> | ||
| template<typename V> requires U | ||
| struct A<int, U>::B { }; | ||
|
|
||
| template<> | ||
| template<typename V> requires true | ||
| void A<int, true>::f() { } | ||
|
|
||
| template<> | ||
| template<typename V> requires true | ||
| constexpr int A<int, true>::x = 1; | ||
|
|
||
| template<> | ||
| template<typename V> requires true | ||
| struct A<int, true>::B { }; | ||
| } // namespace Partial |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,67 +1,107 @@ | ||
| // NOTE: Assertions have been autogenerated by utils/update_cc_test_checks.py UTC_ARGS: --version 5 | ||
| // RUN: %clang_cc1 -O0 -cl-std=CL1.2 -triple amdgcn-amd-amdhsa -emit-llvm %s -o - | FileCheck -check-prefixes=CL12 %s | ||
| // RUN: %clang_cc1 -O0 -cl-std=CL2.0 -triple amdgcn-amd-amdhsa -emit-llvm %s -o - | FileCheck -check-prefixes=CL20 %s | ||
|
|
||
| // CL12-LABEL: define dso_local void @func1( | ||
| // CL12-SAME: ptr addrspace(5) noundef [[X:%.*]]) #[[ATTR0:[0-9]+]] { | ||
| // CL12-NEXT: [[ENTRY:.*:]] | ||
| // CL12-NEXT: [[X_ADDR:%.*]] = alloca ptr addrspace(5), align 4, addrspace(5) | ||
| // CL12-NEXT: store ptr addrspace(5) [[X]], ptr addrspace(5) [[X_ADDR]], align 4 | ||
| // CL12-NEXT: [[TMP0:%.*]] = load ptr addrspace(5), ptr addrspace(5) [[X_ADDR]], align 4 | ||
| // CL12-NEXT: store i32 1, ptr addrspace(5) [[TMP0]], align 4 | ||
| // CL12-NEXT: ret void | ||
| // | ||
| // CL20-LABEL: define dso_local void @func1( | ||
| // CL20-SAME: ptr noundef [[X:%.*]]) #[[ATTR0:[0-9]+]] { | ||
| // CL20-NEXT: [[ENTRY:.*:]] | ||
| // CL20-NEXT: [[X_ADDR:%.*]] = alloca ptr, align 8, addrspace(5) | ||
| // CL20-NEXT: store ptr [[X]], ptr addrspace(5) [[X_ADDR]], align 8 | ||
| // CL20-NEXT: [[TMP0:%.*]] = load ptr, ptr addrspace(5) [[X_ADDR]], align 8 | ||
| // CL20-NEXT: store i32 1, ptr [[TMP0]], align 4 | ||
| // CL20-NEXT: ret void | ||
| // | ||
| void func1(int *x) { | ||
| *x = 1; | ||
| } | ||
|
|
||
| // CL12-LABEL: define dso_local void @func2( | ||
| // CL12-SAME: ) #[[ATTR0]] { | ||
| // CL12-NEXT: [[ENTRY:.*:]] | ||
| // CL12-NEXT: [[LV1:%.*]] = alloca i32, align 4, addrspace(5) | ||
| // CL12-NEXT: [[LV2:%.*]] = alloca i32, align 4, addrspace(5) | ||
| // CL12-NEXT: [[LA:%.*]] = alloca [100 x i32], align 4, addrspace(5) | ||
| // CL12-NEXT: [[LP1:%.*]] = alloca ptr addrspace(5), align 4, addrspace(5) | ||
| // CL12-NEXT: [[LP2:%.*]] = alloca ptr addrspace(5), align 4, addrspace(5) | ||
| // CL12-NEXT: [[LVC:%.*]] = alloca i32, align 4, addrspace(5) | ||
| // CL12-NEXT: store i32 1, ptr addrspace(5) [[LV1]], align 4 | ||
| // CL12-NEXT: store i32 2, ptr addrspace(5) [[LV2]], align 4 | ||
| // CL12-NEXT: [[ARRAYIDX:%.*]] = getelementptr inbounds [100 x i32], ptr addrspace(5) [[LA]], i64 0, i64 0 | ||
| // CL12-NEXT: store i32 3, ptr addrspace(5) [[ARRAYIDX]], align 4 | ||
| // CL12-NEXT: store ptr addrspace(5) [[LV1]], ptr addrspace(5) [[LP1]], align 4 | ||
| // CL12-NEXT: [[ARRAYDECAY:%.*]] = getelementptr inbounds [100 x i32], ptr addrspace(5) [[LA]], i64 0, i64 0 | ||
| // CL12-NEXT: store ptr addrspace(5) [[ARRAYDECAY]], ptr addrspace(5) [[LP2]], align 4 | ||
| // CL12-NEXT: call void @func1(ptr addrspace(5) noundef [[LV1]]) #[[ATTR2:[0-9]+]] | ||
| // CL12-NEXT: store i32 4, ptr addrspace(5) [[LVC]], align 4 | ||
| // CL12-NEXT: store i32 4, ptr addrspace(5) [[LV1]], align 4 | ||
| // CL12-NEXT: ret void | ||
| // | ||
| // CL20-LABEL: define dso_local void @func2( | ||
| // CL20-SAME: ) #[[ATTR0]] { | ||
| // CL20-NEXT: [[ENTRY:.*:]] | ||
| // CL20-NEXT: [[LV1:%.*]] = alloca i32, align 4, addrspace(5) | ||
| // CL20-NEXT: [[LV2:%.*]] = alloca i32, align 4, addrspace(5) | ||
| // CL20-NEXT: [[LA:%.*]] = alloca [100 x i32], align 4, addrspace(5) | ||
| // CL20-NEXT: [[LP1:%.*]] = alloca ptr, align 8, addrspace(5) | ||
| // CL20-NEXT: [[LP2:%.*]] = alloca ptr, align 8, addrspace(5) | ||
| // CL20-NEXT: [[LVC:%.*]] = alloca i32, align 4, addrspace(5) | ||
| // CL20-NEXT: store i32 1, ptr addrspace(5) [[LV1]], align 4 | ||
| // CL20-NEXT: store i32 2, ptr addrspace(5) [[LV2]], align 4 | ||
| // CL20-NEXT: [[ARRAYIDX:%.*]] = getelementptr inbounds [100 x i32], ptr addrspace(5) [[LA]], i64 0, i64 0 | ||
| // CL20-NEXT: store i32 3, ptr addrspace(5) [[ARRAYIDX]], align 4 | ||
| // CL20-NEXT: [[LV1_ASCAST:%.*]] = addrspacecast ptr addrspace(5) [[LV1]] to ptr | ||
| // CL20-NEXT: store ptr [[LV1_ASCAST]], ptr addrspace(5) [[LP1]], align 8 | ||
| // CL20-NEXT: [[ARRAYDECAY:%.*]] = getelementptr inbounds [100 x i32], ptr addrspace(5) [[LA]], i64 0, i64 0 | ||
| // CL20-NEXT: [[ARRAYDECAY_ASCAST:%.*]] = addrspacecast ptr addrspace(5) [[ARRAYDECAY]] to ptr | ||
| // CL20-NEXT: store ptr [[ARRAYDECAY_ASCAST]], ptr addrspace(5) [[LP2]], align 8 | ||
| // CL20-NEXT: [[LV1_ASCAST1:%.*]] = addrspacecast ptr addrspace(5) [[LV1]] to ptr | ||
| // CL20-NEXT: call void @func1(ptr noundef [[LV1_ASCAST1]]) #[[ATTR2:[0-9]+]] | ||
| // CL20-NEXT: store i32 4, ptr addrspace(5) [[LVC]], align 4 | ||
| // CL20-NEXT: store i32 4, ptr addrspace(5) [[LV1]], align 4 | ||
| // CL20-NEXT: ret void | ||
| // | ||
| void func2(void) { | ||
| int lv1; | ||
| lv1 = 1; | ||
|
|
||
| int lv2 = 2; | ||
|
|
||
| int la[100]; | ||
| la[0] = 3; | ||
|
|
||
| int *lp1 = &lv1; | ||
|
|
||
| int *lp2 = la; | ||
|
|
||
| func1(&lv1); | ||
|
|
||
| const int lvc = 4; | ||
| lv1 = lvc; | ||
| } | ||
|
|
||
| // CL12-LABEL: define dso_local void @func3( | ||
| // CL12-SAME: ) #[[ATTR0]] { | ||
| // CL12-NEXT: [[ENTRY:.*:]] | ||
| // CL12-NEXT: [[A:%.*]] = alloca [16 x [1 x float]], align 4, addrspace(5) | ||
| // CL12-NEXT: call void @llvm.memset.p5.i64(ptr addrspace(5) align 4 [[A]], i8 0, i64 64, i1 false) | ||
| // CL12-NEXT: ret void | ||
| // | ||
| // CL20-LABEL: define dso_local void @func3( | ||
| // CL20-SAME: ) #[[ATTR0]] { | ||
| // CL20-NEXT: [[ENTRY:.*:]] | ||
| // CL20-NEXT: [[A:%.*]] = alloca [16 x [1 x float]], align 4, addrspace(5) | ||
| // CL20-NEXT: call void @llvm.memset.p5.i64(ptr addrspace(5) align 4 [[A]], i8 0, i64 64, i1 false) | ||
| // CL20-NEXT: ret void | ||
| // | ||
| void func3(void) { | ||
| float a[16][1] = {{0.}}; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,69 +1,90 @@ | ||
| // RUN: %clang_cc1 -triple spir-unknown-unknown -emit-llvm -cl-ext=+cl_khr_subgroups -O0 -cl-std=clc++ -o - %s | FileCheck --check-prefix=CHECK-SPIR %s | ||
| // RUN: %clang_cc1 -triple %itanium_abi_triple -emit-llvm -cl-ext=+cl_khr_subgroups -O0 -cl-std=clc++ -o - %s | FileCheck %s | ||
| // FIXME: Add MS ABI manglings of OpenCL things and remove %itanium_abi_triple | ||
| // above to support OpenCL in the MS C++ ABI. | ||
|
|
||
| #pragma OPENCL EXTENSION cl_khr_subgroups : enable | ||
|
|
||
| void test1(read_only pipe int p, global int *ptr) { | ||
| // CHECK-SPIR: call spir_func i32 @__read_pipe_2(target("spirv.Pipe", 0) %{{.*}}, ptr addrspace(4) %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call i32 @__read_pipe_2(ptr %{{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| read_pipe(p, ptr); | ||
| // CHECK-SPIR: call spir_func target("spirv.ReserveId") @__reserve_read_pipe(target("spirv.Pipe", 0) %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| // CHECK: call ptr @__reserve_read_pipe(ptr %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| reserve_id_t rid = reserve_read_pipe(p, 2); | ||
| // CHECK-SPIR: call spir_func i32 @__read_pipe_4(target("spirv.Pipe", 0) %{{.*}}, ptr addrspace(4) %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call i32 @__read_pipe_4(ptr %{{.*}}, ptr %{{.*}}, i32 {{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| read_pipe(p, rid, 2, ptr); | ||
| // CHECK-SPIR: call spir_func void @__commit_read_pipe(target("spirv.Pipe", 0) %{{.*}}, target("spirv.ReserveId") %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call void @__commit_read_pipe(ptr %{{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| commit_read_pipe(p, rid); | ||
| } | ||
|
|
||
| void test2(write_only pipe int p, global int *ptr) { | ||
| // CHECK-SPIR: call spir_func i32 @__write_pipe_2(target("spirv.Pipe", 1) %{{.*}}, ptr addrspace(4) %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call i32 @__write_pipe_2(ptr %{{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| write_pipe(p, ptr); | ||
| // CHECK-SPIR: call spir_func target("spirv.ReserveId") @__reserve_write_pipe(target("spirv.Pipe", 1) %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| // CHECK: call ptr @__reserve_write_pipe(ptr %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| reserve_id_t rid = reserve_write_pipe(p, 2); | ||
| // CHECK-SPIR: call spir_func i32 @__write_pipe_4(target("spirv.Pipe", 1) %{{.*}}, ptr addrspace(4) %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call i32 @__write_pipe_4(ptr %{{.*}}, ptr %{{.*}}, i32 {{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| write_pipe(p, rid, 2, ptr); | ||
| // CHECK-SPIR: call spir_func void @__commit_write_pipe(target("spirv.Pipe", 1) %{{.*}}, target("spirv.ReserveId") %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call void @__commit_write_pipe(ptr %{{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| commit_write_pipe(p, rid); | ||
| } | ||
|
|
||
| void test3(read_only pipe int p, global int *ptr) { | ||
| // CHECK-SPIR: call spir_func target("spirv.ReserveId") @__work_group_reserve_read_pipe(target("spirv.Pipe", 0) %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| // CHECK: call ptr @__work_group_reserve_read_pipe(ptr %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| reserve_id_t rid = work_group_reserve_read_pipe(p, 2); | ||
| // CHECK-SPIR: call spir_func void @__work_group_commit_read_pipe(target("spirv.Pipe", 0) %{{.*}}, target("spirv.ReserveId") %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call void @__work_group_commit_read_pipe(ptr %{{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| work_group_commit_read_pipe(p, rid); | ||
| } | ||
|
|
||
| void test4(write_only pipe int p, global int *ptr) { | ||
| // CHECK-SPIR: call spir_func target("spirv.ReserveId") @__work_group_reserve_write_pipe(target("spirv.Pipe", 1) %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| // CHECK: call ptr @__work_group_reserve_write_pipe(ptr %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| reserve_id_t rid = work_group_reserve_write_pipe(p, 2); | ||
| // CHECK-SPIR: call spir_func void @__work_group_commit_write_pipe(target("spirv.Pipe", 1) %{{.*}}, target("spirv.ReserveId") %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call void @__work_group_commit_write_pipe(ptr %{{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| work_group_commit_write_pipe(p, rid); | ||
| } | ||
|
|
||
| void test5(read_only pipe int p, global int *ptr) { | ||
| // CHECK-SPIR: call spir_func target("spirv.ReserveId") @__sub_group_reserve_read_pipe(target("spirv.Pipe", 0) %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| // CHECK: call ptr @__sub_group_reserve_read_pipe(ptr %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| reserve_id_t rid = sub_group_reserve_read_pipe(p, 2); | ||
| // CHECK-SPIR: call spir_func void @__sub_group_commit_read_pipe(target("spirv.Pipe", 0) %{{.*}}, target("spirv.ReserveId") %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call void @__sub_group_commit_read_pipe(ptr %{{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| sub_group_commit_read_pipe(p, rid); | ||
| } | ||
|
|
||
| void test6(write_only pipe int p, global int *ptr) { | ||
| // CHECK-SPIR: call spir_func target("spirv.ReserveId") @__sub_group_reserve_write_pipe(target("spirv.Pipe", 1) %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| // CHECK: call ptr @__sub_group_reserve_write_pipe(ptr %{{.*}}, i32 {{.*}}, i32 4, i32 4) | ||
| reserve_id_t rid = sub_group_reserve_write_pipe(p, 2); | ||
| // CHECK-SPIR: call spir_func void @__sub_group_commit_write_pipe(target("spirv.Pipe", 1) %{{.*}}, target("spirv.ReserveId") %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call void @__sub_group_commit_write_pipe(ptr %{{.*}}, ptr %{{.*}}, i32 4, i32 4) | ||
| sub_group_commit_write_pipe(p, rid); | ||
| } | ||
|
|
||
| void test7(read_only pipe int p, global int *ptr) { | ||
| // CHECK-SPIR: call spir_func i32 @__get_pipe_num_packets_ro(target("spirv.Pipe", 0) %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call i32 @__get_pipe_num_packets_ro(ptr %{{.*}}, i32 4, i32 4) | ||
| *ptr = get_pipe_num_packets(p); | ||
| // CHECK-SPIR: call spir_func i32 @__get_pipe_max_packets_ro(target("spirv.Pipe", 0) %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call i32 @__get_pipe_max_packets_ro(ptr %{{.*}}, i32 4, i32 4) | ||
| *ptr = get_pipe_max_packets(p); | ||
| } | ||
|
|
||
| void test8(write_only pipe int p, global int *ptr) { | ||
| // CHECK-SPIR: call spir_func i32 @__get_pipe_num_packets_wo(target("spirv.Pipe", 1) %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call i32 @__get_pipe_num_packets_wo(ptr %{{.*}}, i32 4, i32 4) | ||
| *ptr = get_pipe_num_packets(p); | ||
| // CHECK-SPIR: call spir_func i32 @__get_pipe_max_packets_wo(target("spirv.Pipe", 1) %{{.*}}, i32 4, i32 4) | ||
| // CHECK: call i32 @__get_pipe_max_packets_wo(ptr %{{.*}}, i32 4, i32 4) | ||
| *ptr = get_pipe_max_packets(p); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,305 @@ | ||
| //===- unittests/Analysis/FlowSensitive/CachedConstAccessorsLatticeTest.cpp ==// | ||
| // | ||
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
| // See https://llvm.org/LICENSE.txt for license information. | ||
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
| // | ||
| //===----------------------------------------------------------------------===// | ||
|
|
||
| #include "clang/Analysis/FlowSensitive/CachedConstAccessorsLattice.h" | ||
|
|
||
| #include <cassert> | ||
| #include <memory> | ||
|
|
||
| #include "clang/AST/Decl.h" | ||
| #include "clang/AST/DeclBase.h" | ||
| #include "clang/AST/DeclCXX.h" | ||
| #include "clang/AST/Expr.h" | ||
| #include "clang/AST/Type.h" | ||
| #include "clang/ASTMatchers/ASTMatchFinder.h" | ||
| #include "clang/ASTMatchers/ASTMatchers.h" | ||
| #include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h" | ||
| #include "clang/Analysis/FlowSensitive/DataflowLattice.h" | ||
| #include "clang/Analysis/FlowSensitive/NoopLattice.h" | ||
| #include "clang/Analysis/FlowSensitive/StorageLocation.h" | ||
| #include "clang/Analysis/FlowSensitive/Value.h" | ||
| #include "clang/Analysis/FlowSensitive/WatchedLiteralsSolver.h" | ||
| #include "clang/Basic/LLVM.h" | ||
| #include "clang/Testing/TestAST.h" | ||
| #include "gmock/gmock.h" | ||
| #include "gtest/gtest.h" | ||
|
|
||
| namespace clang::dataflow { | ||
| namespace { | ||
|
|
||
| using ast_matchers::BoundNodes; | ||
| using ast_matchers::callee; | ||
| using ast_matchers::cxxMemberCallExpr; | ||
| using ast_matchers::functionDecl; | ||
| using ast_matchers::hasName; | ||
| using ast_matchers::match; | ||
| using ast_matchers::selectFirst; | ||
|
|
||
| using dataflow::DataflowAnalysisContext; | ||
| using dataflow::Environment; | ||
| using dataflow::LatticeJoinEffect; | ||
| using dataflow::RecordStorageLocation; | ||
| using dataflow::Value; | ||
| using dataflow::WatchedLiteralsSolver; | ||
|
|
||
| using testing::SizeIs; | ||
|
|
||
| NamedDecl *lookup(StringRef Name, const DeclContext &DC) { | ||
| auto Result = DC.lookup(&DC.getParentASTContext().Idents.get(Name)); | ||
| EXPECT_TRUE(Result.isSingleResult()) << Name; | ||
| return Result.front(); | ||
| } | ||
|
|
||
| class CachedConstAccessorsLatticeTest : public ::testing::Test { | ||
| protected: | ||
| using LatticeT = CachedConstAccessorsLattice<NoopLattice>; | ||
|
|
||
| DataflowAnalysisContext DACtx{std::make_unique<WatchedLiteralsSolver>()}; | ||
| Environment Env{DACtx}; | ||
| }; | ||
|
|
||
| // Basic test AST with two const methods (return a value, and return a ref). | ||
| struct CommonTestInputs { | ||
| CommonTestInputs() | ||
| : AST(R"cpp( | ||
| struct S { | ||
| int *valProperty() const; | ||
| int &refProperty() const; | ||
| }; | ||
| void target() { | ||
| S s; | ||
| s.valProperty(); | ||
| S s2; | ||
| s2.refProperty(); | ||
| } | ||
| )cpp") { | ||
| auto *SDecl = cast<CXXRecordDecl>( | ||
| lookup("S", *AST.context().getTranslationUnitDecl())); | ||
| SType = AST.context().getRecordType(SDecl); | ||
| CallVal = selectFirst<CallExpr>( | ||
| "call", | ||
| match(cxxMemberCallExpr(callee(functionDecl(hasName("valProperty")))) | ||
| .bind("call"), | ||
| AST.context())); | ||
| assert(CallVal != nullptr); | ||
|
|
||
| CallRef = selectFirst<CallExpr>( | ||
| "call", | ||
| match(cxxMemberCallExpr(callee(functionDecl(hasName("refProperty")))) | ||
| .bind("call"), | ||
| AST.context())); | ||
| assert(CallRef != nullptr); | ||
| } | ||
|
|
||
| TestAST AST; | ||
| QualType SType; | ||
| const CallExpr *CallVal; | ||
| const CallExpr *CallRef; | ||
| }; | ||
|
|
||
| TEST_F(CachedConstAccessorsLatticeTest, | ||
| SamePrimitiveValBeforeClearOrDiffAfterClear) { | ||
| CommonTestInputs Inputs; | ||
| auto *CE = Inputs.CallVal; | ||
| RecordStorageLocation Loc(Inputs.SType, RecordStorageLocation::FieldToLoc(), | ||
| {}); | ||
|
|
||
| LatticeT Lattice; | ||
| Value *Val1 = Lattice.getOrCreateConstMethodReturnValue(Loc, CE, Env); | ||
| Value *Val2 = Lattice.getOrCreateConstMethodReturnValue(Loc, CE, Env); | ||
|
|
||
| EXPECT_EQ(Val1, Val2); | ||
|
|
||
| Lattice.clearConstMethodReturnValues(Loc); | ||
| Value *Val3 = Lattice.getOrCreateConstMethodReturnValue(Loc, CE, Env); | ||
|
|
||
| EXPECT_NE(Val3, Val1); | ||
| EXPECT_NE(Val3, Val2); | ||
| } | ||
|
|
||
| TEST_F(CachedConstAccessorsLatticeTest, SameLocBeforeClearOrDiffAfterClear) { | ||
| CommonTestInputs Inputs; | ||
| auto *CE = Inputs.CallRef; | ||
| RecordStorageLocation Loc(Inputs.SType, RecordStorageLocation::FieldToLoc(), | ||
| {}); | ||
|
|
||
| LatticeT Lattice; | ||
| auto NopInit = [](StorageLocation &) {}; | ||
| StorageLocation *Loc1 = Lattice.getOrCreateConstMethodReturnStorageLocation( | ||
| Loc, CE, Env, NopInit); | ||
| auto NotCalled = [](StorageLocation &) { | ||
| ASSERT_TRUE(false) << "Not reached"; | ||
| }; | ||
| StorageLocation *Loc2 = Lattice.getOrCreateConstMethodReturnStorageLocation( | ||
| Loc, CE, Env, NotCalled); | ||
|
|
||
| EXPECT_EQ(Loc1, Loc2); | ||
|
|
||
| Lattice.clearConstMethodReturnStorageLocations(Loc); | ||
| StorageLocation *Loc3 = Lattice.getOrCreateConstMethodReturnStorageLocation( | ||
| Loc, CE, Env, NopInit); | ||
|
|
||
| EXPECT_NE(Loc3, Loc1); | ||
| EXPECT_NE(Loc3, Loc2); | ||
| } | ||
|
|
||
| TEST_F(CachedConstAccessorsLatticeTest, | ||
| SameStructValBeforeClearOrDiffAfterClear) { | ||
| TestAST AST(R"cpp( | ||
| struct S { | ||
| S structValProperty() const; | ||
| }; | ||
| void target() { | ||
| S s; | ||
| s.structValProperty(); | ||
| } | ||
| )cpp"); | ||
| auto *SDecl = | ||
| cast<CXXRecordDecl>(lookup("S", *AST.context().getTranslationUnitDecl())); | ||
| QualType SType = AST.context().getRecordType(SDecl); | ||
| const CallExpr *CE = selectFirst<CallExpr>( | ||
| "call", match(cxxMemberCallExpr( | ||
| callee(functionDecl(hasName("structValProperty")))) | ||
| .bind("call"), | ||
| AST.context())); | ||
| ASSERT_NE(CE, nullptr); | ||
|
|
||
| RecordStorageLocation Loc(SType, RecordStorageLocation::FieldToLoc(), {}); | ||
|
|
||
| LatticeT Lattice; | ||
| // Accessors that return a record by value are modeled by a record storage | ||
| // location (instead of a Value). | ||
| auto NopInit = [](StorageLocation &) {}; | ||
| StorageLocation *Loc1 = Lattice.getOrCreateConstMethodReturnStorageLocation( | ||
| Loc, CE, Env, NopInit); | ||
| auto NotCalled = [](StorageLocation &) { | ||
| ASSERT_TRUE(false) << "Not reached"; | ||
| }; | ||
| StorageLocation *Loc2 = Lattice.getOrCreateConstMethodReturnStorageLocation( | ||
| Loc, CE, Env, NotCalled); | ||
|
|
||
| EXPECT_EQ(Loc1, Loc2); | ||
|
|
||
| Lattice.clearConstMethodReturnStorageLocations(Loc); | ||
| StorageLocation *Loc3 = Lattice.getOrCreateConstMethodReturnStorageLocation( | ||
| Loc, CE, Env, NopInit); | ||
|
|
||
| EXPECT_NE(Loc3, Loc1); | ||
| EXPECT_NE(Loc3, Loc1); | ||
| } | ||
|
|
||
| TEST_F(CachedConstAccessorsLatticeTest, ClearDifferentLocs) { | ||
| CommonTestInputs Inputs; | ||
| auto *CE = Inputs.CallRef; | ||
| RecordStorageLocation LocS1(Inputs.SType, RecordStorageLocation::FieldToLoc(), | ||
| {}); | ||
| RecordStorageLocation LocS2(Inputs.SType, RecordStorageLocation::FieldToLoc(), | ||
| {}); | ||
|
|
||
| LatticeT Lattice; | ||
| auto NopInit = [](StorageLocation &) {}; | ||
| StorageLocation *RetLoc1 = | ||
| Lattice.getOrCreateConstMethodReturnStorageLocation(LocS1, CE, Env, | ||
| NopInit); | ||
| Lattice.clearConstMethodReturnStorageLocations(LocS2); | ||
| auto NotCalled = [](StorageLocation &) { | ||
| ASSERT_TRUE(false) << "Not reached"; | ||
| }; | ||
| StorageLocation *RetLoc2 = | ||
| Lattice.getOrCreateConstMethodReturnStorageLocation(LocS1, CE, Env, | ||
| NotCalled); | ||
|
|
||
| EXPECT_EQ(RetLoc1, RetLoc2); | ||
| } | ||
|
|
||
| TEST_F(CachedConstAccessorsLatticeTest, DifferentValsFromDifferentLocs) { | ||
| TestAST AST(R"cpp( | ||
| struct S { | ||
| int *valProperty() const; | ||
| }; | ||
| void target() { | ||
| S s1; | ||
| s1.valProperty(); | ||
| S s2; | ||
| s2.valProperty(); | ||
| } | ||
| )cpp"); | ||
| auto *SDecl = | ||
| cast<CXXRecordDecl>(lookup("S", *AST.context().getTranslationUnitDecl())); | ||
| QualType SType = AST.context().getRecordType(SDecl); | ||
| SmallVector<BoundNodes, 1> valPropertyCalls = | ||
| match(cxxMemberCallExpr(callee(functionDecl(hasName("valProperty")))) | ||
| .bind("call"), | ||
| AST.context()); | ||
| ASSERT_THAT(valPropertyCalls, SizeIs(2)); | ||
|
|
||
| const CallExpr *CE1 = selectFirst<CallExpr>("call", valPropertyCalls); | ||
| ASSERT_NE(CE1, nullptr); | ||
|
|
||
| valPropertyCalls.erase(valPropertyCalls.begin()); | ||
| const CallExpr *CE2 = selectFirst<CallExpr>("call", valPropertyCalls); | ||
| ASSERT_NE(CE2, nullptr); | ||
| ASSERT_NE(CE1, CE2); | ||
|
|
||
| RecordStorageLocation LocS1(SType, RecordStorageLocation::FieldToLoc(), {}); | ||
| RecordStorageLocation LocS2(SType, RecordStorageLocation::FieldToLoc(), {}); | ||
|
|
||
| LatticeT Lattice; | ||
| Value *Val1 = Lattice.getOrCreateConstMethodReturnValue(LocS1, CE1, Env); | ||
| Value *Val2 = Lattice.getOrCreateConstMethodReturnValue(LocS2, CE2, Env); | ||
|
|
||
| EXPECT_NE(Val1, Val2); | ||
| } | ||
|
|
||
| TEST_F(CachedConstAccessorsLatticeTest, JoinSameNoop) { | ||
| CommonTestInputs Inputs; | ||
| auto *CE = Inputs.CallVal; | ||
| RecordStorageLocation Loc(Inputs.SType, RecordStorageLocation::FieldToLoc(), | ||
| {}); | ||
|
|
||
| LatticeT EmptyLattice; | ||
| LatticeT EmptyLattice2; | ||
| EXPECT_EQ(EmptyLattice.join(EmptyLattice2), LatticeJoinEffect::Unchanged); | ||
|
|
||
| LatticeT Lattice1; | ||
| Lattice1.getOrCreateConstMethodReturnValue(Loc, CE, Env); | ||
| EXPECT_EQ(Lattice1.join(Lattice1), LatticeJoinEffect::Unchanged); | ||
| } | ||
|
|
||
| TEST_F(CachedConstAccessorsLatticeTest, ProducesNewValueAfterJoinDistinct) { | ||
| CommonTestInputs Inputs; | ||
| auto *CE = Inputs.CallVal; | ||
| RecordStorageLocation Loc(Inputs.SType, RecordStorageLocation::FieldToLoc(), | ||
| {}); | ||
|
|
||
| // L1 w/ v vs L2 empty | ||
| LatticeT Lattice1; | ||
| Value *Val1 = Lattice1.getOrCreateConstMethodReturnValue(Loc, CE, Env); | ||
|
|
||
| LatticeT EmptyLattice; | ||
|
|
||
| EXPECT_EQ(Lattice1.join(EmptyLattice), LatticeJoinEffect::Changed); | ||
| Value *ValAfterJoin = | ||
| Lattice1.getOrCreateConstMethodReturnValue(Loc, CE, Env); | ||
|
|
||
| EXPECT_NE(ValAfterJoin, Val1); | ||
|
|
||
| // L1 w/ v1 vs L3 w/ v2 | ||
| LatticeT Lattice3; | ||
| Value *Val3 = Lattice3.getOrCreateConstMethodReturnValue(Loc, CE, Env); | ||
|
|
||
| EXPECT_EQ(Lattice1.join(Lattice3), LatticeJoinEffect::Changed); | ||
| Value *ValAfterJoin2 = | ||
| Lattice1.getOrCreateConstMethodReturnValue(Loc, CE, Env); | ||
|
|
||
| EXPECT_NE(ValAfterJoin2, ValAfterJoin); | ||
| EXPECT_NE(ValAfterJoin2, Val3); | ||
| } | ||
|
|
||
| } // namespace | ||
| } // namespace clang::dataflow |