| //===-- lib/Semantics/check-omp-atomic.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 |
| // |
| //===----------------------------------------------------------------------===// |
| // |
| // Semantic checks related to the ATOMIC construct. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| #include "check-omp-structure.h" |
| |
| #include "flang/Common/indirection.h" |
| #include "flang/Common/template.h" |
| #include "flang/Evaluate/expression.h" |
| #include "flang/Evaluate/match.h" |
| #include "flang/Evaluate/rewrite.h" |
| #include "flang/Evaluate/tools.h" |
| #include "flang/Parser/char-block.h" |
| #include "flang/Parser/parse-tree.h" |
| #include "flang/Semantics/openmp-utils.h" |
| #include "flang/Semantics/symbol.h" |
| #include "flang/Semantics/tools.h" |
| #include "flang/Semantics/type.h" |
| |
| #include "llvm/ADT/ArrayRef.h" |
| #include "llvm/ADT/STLExtras.h" |
| #include "llvm/Frontend/OpenMP/OMP.h" |
| #include "llvm/Support/ErrorHandling.h" |
| |
| #include <cassert> |
| #include <list> |
| #include <optional> |
| #include <string_view> |
| #include <tuple> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| namespace Fortran::semantics { |
| |
| using namespace Fortran::semantics::omp; |
| |
| namespace operation = Fortran::evaluate::operation; |
| |
| static MaybeExpr PostSemaRewrite(const SomeExpr &atom, const SomeExpr &expr); |
| |
| template <typename T, typename U> |
| static bool operator!=(const evaluate::Expr<T> &e, const evaluate::Expr<U> &f) { |
| return !(e == f); |
| } |
| |
| namespace { |
| template <typename...> struct IsIntegral { |
| static constexpr bool value{false}; |
| }; |
| |
| template <common::TypeCategory C, int K> |
| struct IsIntegral<evaluate::Type<C, K>> { |
| static constexpr bool value{// |
| C == common::TypeCategory::Integer || |
| C == common::TypeCategory::Unsigned}; |
| }; |
| |
| template <typename T> constexpr bool is_integral_v{IsIntegral<T>::value}; |
| |
| template <typename...> struct IsFloatingPoint { |
| static constexpr bool value{false}; |
| }; |
| |
| template <common::TypeCategory C, int K> |
| struct IsFloatingPoint<evaluate::Type<C, K>> { |
| static constexpr bool value{// |
| C == common::TypeCategory::Real || C == common::TypeCategory::Complex}; |
| }; |
| |
| template <typename T> |
| constexpr bool is_floating_point_v{IsFloatingPoint<T>::value}; |
| |
| template <typename T> |
| constexpr bool is_numeric_v{is_integral_v<T> || is_floating_point_v<T>}; |
| |
| template <typename...> struct IsLogical { |
| static constexpr bool value{false}; |
| }; |
| |
| template <common::TypeCategory C, int K> |
| struct IsLogical<evaluate::Type<C, K>> { |
| static constexpr bool value{C == common::TypeCategory::Logical}; |
| }; |
| |
| template <typename T> constexpr bool is_logical_v{IsLogical<T>::value}; |
| |
| template <typename T, typename Op0, typename Op1> |
| using ReassocOpBase = evaluate::match::AnyOfPattern< // |
| evaluate::match::Add<T, Op0, Op1>, // |
| evaluate::match::Mul<T, Op0, Op1>, // |
| evaluate::match::LogicalOp<common::LogicalOperator::And, T, Op0, Op1>, |
| evaluate::match::LogicalOp<common::LogicalOperator::Or, T, Op0, Op1>, |
| evaluate::match::LogicalOp<common::LogicalOperator::Eqv, T, Op0, Op1>, |
| evaluate::match::LogicalOp<common::LogicalOperator::Neqv, T, Op0, Op1>>; |
| |
| template <typename T, typename Op0, typename Op1> |
| struct ReassocOp : public ReassocOpBase<T, Op0, Op1> { |
| using Base = ReassocOpBase<T, Op0, Op1>; |
| using Base::Base; |
| }; |
| |
| template <typename T, typename Op0, typename Op1> |
| ReassocOp<T, Op0, Op1> reassocOp(const Op0 &op0, const Op1 &op1) { |
| return ReassocOp<T, Op0, Op1>(op0, op1); |
| } |
| } // namespace |
| |
| struct ReassocRewriter : public evaluate::rewrite::Identity { |
| using Id = evaluate::rewrite::Identity; |
| struct NonIntegralTag {}; |
| |
| ReassocRewriter(const SomeExpr &atom, const SemanticsContext &context) |
| : atom_(atom), context_(context) {} |
| |
| // Try to find cases where the input expression is of the form |
| // (1) (a . b) . c, or |
| // (2) a . (b . c), |
| // where . denotes an associative operation, and a, b, c are some |
| // subexpresions. |
| // If one of the operands in the nested operation is the atomic variable |
| // (with some possible type conversions applied to it), bring it to the |
| // top-level operation, and move the top-level operand into the nested |
| // operation. |
| // For example, assuming x is the atomic variable: |
| // (a + x) + b -> (a + b) + x, i.e. (conceptually) swap x and b. |
| template <typename T, typename U, |
| typename = std::enable_if_t<is_numeric_v<T> || is_logical_v<T>>> |
| evaluate::Expr<T> operator()(evaluate::Expr<T> &&x, const U &u) { |
| if constexpr (is_floating_point_v<T>) { |
| if (!context_.langOptions().AssociativeMath) { |
| return Id::operator()(std::move(x), u); |
| } |
| } |
| // As per the above comment, there are 3 subexpressions involved in this |
| // transformation. A match::Expr<T> will match evaluate::Expr<U> when T is |
| // same as U, plus it will store a pointer (ref) to the matched expression. |
| // When the match is successful, the sub[i].ref will point to a, b, x (in |
| // some order) from the example above. |
| evaluate::match::Expr<T> sub[3]; |
| auto inner{reassocOp<T>(sub[0], sub[1])}; |
| auto outer1{reassocOp<T>(inner, sub[2])}; // inner . something |
| auto outer2{reassocOp<T>(sub[2], inner)}; // something . inner |
| #if !defined(__clang__) && !defined(_MSC_VER) && \ |
| (__GNUC__ < 8 || (__GNUC__ == 8 && __GNUC_MINOR__ < 5)) |
| // If GCC version < 8.5, use this definition. For the other definition |
| // (which is equivalent), GCC 7.5 emits a somewhat cryptic error: |
| // use of ‘outer1’ before deduction of ‘auto’ |
| // inside of the visitor function in common::visit. |
| // Since this works with clang, MSVC and at least GCC 8.5, I'm assuming |
| // that this is some kind of a GCC issue. |
| using MatchTypes = std::tuple<evaluate::Add<T>, evaluate::Multiply<T>, |
| evaluate::LogicalOperation<T::kind>>; |
| #else |
| using MatchTypes = typename decltype(outer1)::MatchTypes; |
| #endif |
| // There is no way to ensure that the outer operation is the same as |
| // the inner one. They are matched independently, so we need to compare |
| // the index in the member variant that represents the matched type. |
| if ((match(outer1, x) && outer1.ref.index() == inner.ref.index()) || |
| (match(outer2, x) && outer2.ref.index() == inner.ref.index())) { |
| size_t atomIdx{[&]() { // sub[atomIdx] will be the atom. |
| size_t idx; |
| for (idx = 0; idx != 3; ++idx) { |
| if (IsAtom(*sub[idx].ref)) { |
| break; |
| } |
| } |
| return idx; |
| }()}; |
| |
| if (atomIdx > 2) { |
| return Id::operator()(std::move(x), u); |
| } |
| return common::visit( |
| [&](auto &&s) { |
| // Build the new expression from the matched components. |
| return Reconstruct<T, MatchTypes>(s, *sub[atomIdx].ref, |
| *sub[(atomIdx + 1) % 3].ref, *sub[(atomIdx + 2) % 3].ref); |
| }, |
| evaluate::match::deparen(x).u); |
| } |
| return Id::operator()(std::move(x), u); |
| } |
| |
| template <typename T, typename U, |
| typename = std::enable_if_t<!is_numeric_v<T> && !is_logical_v<T>>> |
| evaluate::Expr<T> operator()( |
| evaluate::Expr<T> &&x, const U &u, NonIntegralTag = {}) { |
| return Id::operator()(std::move(x), u); |
| } |
| |
| private: |
| template <typename T, typename MatchTypes, typename S> |
| evaluate::Expr<T> Reconstruct(const S &op, evaluate::Expr<T> atom, |
| evaluate::Expr<T> op1, evaluate::Expr<T> op2) { |
| using TypeS = llvm::remove_cvref_t<decltype(op)>; |
| // This function has to be semantically correct for all possible types |
| // of S even though at runtime s will only be one of the matched types. |
| // Limit the construction to the operation types that we tried to match |
| // (otherwise TypeS(op1, op2) would fail for non-binary operations). |
| if constexpr (!common::HasMember<TypeS, MatchTypes>) { |
| return evaluate::Expr<T>(TypeS(op)); |
| } else if constexpr (is_logical_v<T>) { |
| constexpr int K{T::kind}; |
| if constexpr (std::is_same_v<TypeS, evaluate::LogicalOperation<K>>) { |
| // Logical operators take an extra argument in their constructor, |
| // so they need their own reconstruction code. |
| common::LogicalOperator opCode{op.logicalOperator}; |
| return evaluate::Expr<T>(TypeS( // |
| opCode, std::move(atom), |
| evaluate::Expr<T>(TypeS( // |
| opCode, std::move(op1), std::move(op2))))); |
| } |
| } else { |
| // Generic reconstruction. |
| return evaluate::Expr<T>(TypeS( // |
| std::move(atom), |
| evaluate::Expr<T>(TypeS( // |
| std::move(op1), std::move(op2))))); |
| } |
| } |
| |
| template <typename T> bool IsAtom(const evaluate::Expr<T> &x) const { |
| return IsSameOrConvertOf(evaluate::AsGenericExpr(AsRvalue(x)), atom_); |
| } |
| |
| const SomeExpr &atom_; |
| const SemanticsContext &context_; |
| }; |
| |
| struct AnalyzedCondStmt { |
| SomeExpr cond{evaluate::NullPointer{}}; // Default ctor is deleted |
| parser::CharBlock source; |
| SourcedActionStmt ift, iff; |
| }; |
| |
| // Compute the `evaluate::Assignment` from parser::ActionStmt. The assumption |
| // is that the ActionStmt will be either an assignment or a pointer-assignment, |
| // otherwise return std::nullopt. |
| // Note: This function can return std::nullopt on [Pointer]AssignmentStmt where |
| // the "typedAssignment" is unset. This can happen if there are semantic errors |
| // in the purported assignment. |
| static std::optional<evaluate::Assignment> GetEvaluateAssignment( |
| const parser::ActionStmt *x) { |
| if (x == nullptr) { |
| return std::nullopt; |
| } |
| |
| using AssignmentStmt = common::Indirection<parser::AssignmentStmt>; |
| using PointerAssignmentStmt = |
| common::Indirection<parser::PointerAssignmentStmt>; |
| using TypedAssignment = parser::AssignmentStmt::TypedAssignment; |
| |
| return common::visit( |
| [](auto &&s) -> std::optional<evaluate::Assignment> { |
| using BareS = llvm::remove_cvref_t<decltype(s)>; |
| if constexpr (std::is_same_v<BareS, AssignmentStmt> || |
| std::is_same_v<BareS, PointerAssignmentStmt>) { |
| const TypedAssignment &typed{s.value().typedAssignment}; |
| // ForwardOwningPointer typedAssignment |
| // `- GenericAssignmentWrapper ^.get() |
| // `- std::optional<Assignment> ^->v |
| return typed.get()->v; |
| } else { |
| return std::nullopt; |
| } |
| }, |
| x->u); |
| } |
| |
| static std::optional<AnalyzedCondStmt> AnalyzeConditionalStmt( |
| const parser::ExecutionPartConstruct *x) { |
| if (x == nullptr) { |
| return std::nullopt; |
| } |
| |
| // Extract the evaluate::Expr from ScalarLogicalExpr. |
| auto getFromLogical{[](const parser::ScalarLogicalExpr &logical) { |
| // ScalarLogicalExpr is Scalar<Logical<common::Indirection<Expr>>> |
| const parser::Expr &expr{logical.thing.thing.value()}; |
| return GetEvaluateExpr(expr); |
| }}; |
| |
| // Recognize either |
| // ExecutionPartConstruct -> ExecutableConstruct -> ActionStmt -> IfStmt, or |
| // ExecutionPartConstruct -> ExecutableConstruct -> IfConstruct. |
| |
| if (auto &&action{GetActionStmt(x)}) { |
| if (auto *ifs{std::get_if<common::Indirection<parser::IfStmt>>( |
| &action.stmt->u)}) { |
| const parser::IfStmt &s{ifs->value()}; |
| auto &&maybeCond{ |
| getFromLogical(std::get<parser::ScalarLogicalExpr>(s.t))}; |
| auto &thenStmt{ |
| std::get<parser::UnlabeledStatement<parser::ActionStmt>>(s.t)}; |
| if (maybeCond) { |
| return AnalyzedCondStmt{std::move(*maybeCond), action.source, |
| SourcedActionStmt{&thenStmt.statement, thenStmt.source}, |
| SourcedActionStmt{}}; |
| } |
| } |
| return std::nullopt; |
| } |
| |
| if (auto *exec{std::get_if<parser::ExecutableConstruct>(&x->u)}) { |
| if (auto *ifc{ |
| std::get_if<common::Indirection<parser::IfConstruct>>(&exec->u)}) { |
| using ElseBlock = parser::IfConstruct::ElseBlock; |
| using ElseIfBlock = parser::IfConstruct::ElseIfBlock; |
| const parser::IfConstruct &s{ifc->value()}; |
| |
| if (!std::get<std::list<ElseIfBlock>>(s.t).empty()) { |
| // Not expecting any else-if statements. |
| return std::nullopt; |
| } |
| auto &stmt{std::get<parser::Statement<parser::IfThenStmt>>(s.t)}; |
| auto &&maybeCond{getFromLogical( |
| std::get<parser::ScalarLogicalExpr>(stmt.statement.t))}; |
| if (!maybeCond) { |
| return std::nullopt; |
| } |
| |
| if (auto &maybeElse{std::get<std::optional<ElseBlock>>(s.t)}) { |
| AnalyzedCondStmt result{std::move(*maybeCond), stmt.source, |
| GetActionStmt(std::get<parser::Block>(s.t)), |
| GetActionStmt(std::get<parser::Block>(maybeElse->t))}; |
| if (result.ift.stmt && result.iff.stmt) { |
| return result; |
| } |
| } else { |
| AnalyzedCondStmt result{std::move(*maybeCond), stmt.source, |
| GetActionStmt(std::get<parser::Block>(s.t)), SourcedActionStmt{}}; |
| if (result.ift.stmt) { |
| return result; |
| } |
| } |
| } |
| return std::nullopt; |
| } |
| |
| return std::nullopt; |
| } |
| |
| static std::pair<parser::CharBlock, parser::CharBlock> SplitAssignmentSource( |
| parser::CharBlock source) { |
| // Find => in the range, if not found, find = that is not a part of |
| // <=, >=, ==, or /=. |
| auto trim{[](std::string_view v) { |
| const char *begin{v.data()}; |
| const char *end{begin + v.size()}; |
| while (*begin == ' ' && begin != end) { |
| ++begin; |
| } |
| while (begin != end && end[-1] == ' ') { |
| --end; |
| } |
| assert(begin != end && "Source should not be empty"); |
| return parser::CharBlock(begin, end - begin); |
| }}; |
| |
| std::string_view sv(source.begin(), source.size()); |
| |
| if (auto where{sv.find("=>")}; where != sv.npos) { |
| std::string_view lhs(sv.data(), where); |
| std::string_view rhs(sv.data() + where + 2, sv.size() - where - 2); |
| return std::make_pair(trim(lhs), trim(rhs)); |
| } |
| |
| // Go backwards, since all the exclusions above end with a '='. |
| for (size_t next{source.size()}; next > 1; --next) { |
| if (sv[next - 1] == '=' && !llvm::is_contained("<>=/", sv[next - 2])) { |
| std::string_view lhs(sv.data(), next - 1); |
| std::string_view rhs(sv.data() + next, sv.size() - next); |
| return std::make_pair(trim(lhs), trim(rhs)); |
| } |
| } |
| llvm_unreachable("Could not find assignment operator"); |
| } |
| |
| static std::vector<SomeExpr> GetNonAtomExpressions( |
| const SomeExpr &atom, const std::vector<SomeExpr> &exprs) { |
| std::vector<SomeExpr> nonAtom; |
| for (const SomeExpr &e : exprs) { |
| if (!IsSameOrConvertOf(e, atom)) { |
| nonAtom.push_back(e); |
| } |
| } |
| return nonAtom; |
| } |
| |
| static std::vector<SomeExpr> GetNonAtomArguments( |
| const SomeExpr &atom, const SomeExpr &expr) { |
| if (auto &&maybe{GetConvertInput(expr)}) { |
| return GetNonAtomExpressions( |
| atom, GetTopLevelOperationIgnoreResizing(*maybe).second); |
| } |
| return {}; |
| } |
| |
| static bool IsCheckForAssociated(const SomeExpr &cond) { |
| return GetTopLevelOperationIgnoreResizing(cond).first == |
| operation::Operator::Associated; |
| } |
| |
| static bool IsMaybeAtomicWrite(const evaluate::Assignment &assign) { |
| // This ignores function calls, so it will accept "f(x) = f(x) + 1" |
| // for example. |
| return HasStorageOverlap(assign.lhs, assign.rhs) == nullptr; |
| } |
| |
| static void SetExpr(parser::TypedExpr &expr, MaybeExpr value) { |
| if (value) { |
| expr.Reset(new evaluate::GenericExprWrapper(std::move(value)), |
| evaluate::GenericExprWrapper::Deleter); |
| } |
| } |
| |
| static void SetAssignment(parser::AssignmentStmt::TypedAssignment &assign, |
| std::optional<evaluate::Assignment> value) { |
| if (value) { |
| assign.Reset(new evaluate::GenericAssignmentWrapper(std::move(value)), |
| evaluate::GenericAssignmentWrapper::Deleter); |
| } |
| } |
| |
| namespace { |
| struct AtomicAnalysis { |
| AtomicAnalysis(const SomeExpr &atom, const MaybeExpr &cond = std::nullopt) |
| : atom_(atom), cond_(cond) {} |
| |
| AtomicAnalysis &addOp0(int what, |
| const std::optional<evaluate::Assignment> &maybeAssign = std::nullopt) { |
| return addOp(op0_, what, maybeAssign); |
| } |
| AtomicAnalysis &addOp1(int what, |
| const std::optional<evaluate::Assignment> &maybeAssign = std::nullopt) { |
| return addOp(op1_, what, maybeAssign); |
| } |
| |
| operator parser::OpenMPAtomicConstruct::Analysis() const { |
| // Defined in flang/include/flang/Parser/parse-tree.h |
| // |
| // struct Analysis { |
| // struct Kind { |
| // static constexpr int None = 0; |
| // static constexpr int Read = 1; |
| // static constexpr int Write = 2; |
| // static constexpr int Update = Read | Write; |
| // static constexpr int Action = 3; // Bits containing None, Read, |
| // // Write, Update |
| // static constexpr int IfTrue = 4; |
| // static constexpr int IfFalse = 8; |
| // static constexpr int Condition = 12; // Bits containing IfTrue, |
| // // IfFalse |
| // }; |
| // struct Op { |
| // int what; |
| // TypedAssignment assign; |
| // }; |
| // TypedExpr atom, cond; |
| // Op op0, op1; |
| // }; |
| |
| parser::OpenMPAtomicConstruct::Analysis an; |
| SetExpr(an.atom, atom_); |
| SetExpr(an.cond, cond_); |
| an.op0 = std::move(op0_); |
| an.op1 = std::move(op1_); |
| return an; |
| } |
| |
| private: |
| struct Op { |
| operator parser::OpenMPAtomicConstruct::Analysis::Op() const { |
| parser::OpenMPAtomicConstruct::Analysis::Op op; |
| op.what = what; |
| SetAssignment(op.assign, assign); |
| return op; |
| } |
| |
| int what; |
| std::optional<evaluate::Assignment> assign; |
| }; |
| |
| AtomicAnalysis &addOp(Op &op, int what, |
| const std::optional<evaluate::Assignment> &maybeAssign) { |
| op.what = what; |
| if (maybeAssign) { |
| if (MaybeExpr rewritten{PostSemaRewrite(atom_, maybeAssign->rhs)}) { |
| op.assign = evaluate::Assignment( |
| AsRvalue(maybeAssign->lhs), std::move(*rewritten)); |
| op.assign->u = std::move(maybeAssign->u); |
| } else { |
| op.assign = *maybeAssign; |
| } |
| } |
| return *this; |
| } |
| |
| const SomeExpr &atom_; |
| const MaybeExpr &cond_; |
| Op op0_, op1_; |
| }; |
| } // namespace |
| |
| /// Check if `expr` satisfies the following conditions for x and v: |
| /// |
| /// [6.0:189:10-12] |
| /// - x and v (as applicable) are either scalar variables or |
| /// function references with scalar data pointer result of non-character |
| /// intrinsic type or variables that are non-polymorphic scalar pointers |
| /// and any length type parameter must be constant. |
| void OmpStructureChecker::CheckAtomicType( |
| SymbolRef sym, parser::CharBlock source, std::string_view name) { |
| const DeclTypeSpec *typeSpec{sym->GetType()}; |
| if (!typeSpec) { |
| return; |
| } |
| |
| if (!IsPointer(sym)) { |
| using Category = DeclTypeSpec::Category; |
| Category cat{typeSpec->category()}; |
| if (cat == Category::Character) { |
| context_.Say(source, |
| "Atomic variable %s cannot have CHARACTER type"_err_en_US, name); |
| } else if (cat != Category::Numeric && cat != Category::Logical) { |
| context_.Say(source, |
| "Atomic variable %s should have an intrinsic type"_err_en_US, name); |
| } |
| return; |
| } |
| |
| // Variable is a pointer. |
| if (typeSpec->IsPolymorphic()) { |
| context_.Say(source, |
| "Atomic variable %s cannot be a pointer to a polymorphic type"_err_en_US, |
| name); |
| return; |
| } |
| |
| // Go over all length parameters, if any, and check if they are |
| // explicit. |
| if (const DerivedTypeSpec *derived{typeSpec->AsDerived()}) { |
| if (llvm::any_of(derived->parameters(), [](auto &&entry) { |
| // "entry" is a map entry |
| return entry.second.isLen() && !entry.second.isExplicit(); |
| })) { |
| context_.Say(source, |
| "Atomic variable %s is a pointer to a type with non-constant length parameter"_err_en_US, |
| name); |
| } |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicVariable( |
| const SomeExpr &atom, parser::CharBlock source) { |
| if (atom.Rank() != 0) { |
| context_.Say(source, "Atomic variable %s should be a scalar"_err_en_US, |
| atom.AsFortran()); |
| } |
| |
| std::vector<SomeExpr> dsgs{GetAllDesignators(atom)}; |
| assert(dsgs.size() == 1 && "Should have a single top-level designator"); |
| evaluate::SymbolVector syms{evaluate::GetSymbolVector(dsgs.front())}; |
| |
| CheckAtomicType(syms.back(), source, atom.AsFortran()); |
| |
| if (IsAllocatable(syms.back()) && !IsArrayElement(atom)) { |
| context_.Say(source, "Atomic variable %s cannot be ALLOCATABLE"_err_en_US, |
| atom.AsFortran()); |
| } |
| } |
| |
| void OmpStructureChecker::CheckStorageOverlap(const SomeExpr &base, |
| llvm::ArrayRef<evaluate::Expr<evaluate::SomeType>> exprs, |
| parser::CharBlock source) { |
| if (auto *expr{HasStorageOverlap(base, exprs)}) { |
| context_.Say(source, |
| "Within atomic operation %s and %s access the same storage"_warn_en_US, |
| base.AsFortran(), expr->AsFortran()); |
| } |
| } |
| |
| void OmpStructureChecker::ErrorShouldBeVariable( |
| const MaybeExpr &expr, parser::CharBlock source) { |
| if (expr) { |
| context_.Say(source, "Atomic expression %s should be a variable"_err_en_US, |
| expr->AsFortran()); |
| } else { |
| context_.Say(source, "Atomic expression should be a variable"_err_en_US); |
| } |
| } |
| |
| std::pair<const parser::ExecutionPartConstruct *, |
| const parser::ExecutionPartConstruct *> |
| OmpStructureChecker::CheckUpdateCapture( |
| const parser::ExecutionPartConstruct *ec1, |
| const parser::ExecutionPartConstruct *ec2, parser::CharBlock source) { |
| // Decide which statement is the atomic update and which is the capture. |
| // |
| // The two allowed cases are: |
| // x = ... atomic-var = ... |
| // ... = x capture-var = atomic-var (with optional converts) |
| // or |
| // ... = x capture-var = atomic-var (with optional converts) |
| // x = ... atomic-var = ... |
| // |
| // The case of 'a = b; b = a' is ambiguous, so pick the first one as capture |
| // (which makes more sense, as it captures the original value of the atomic |
| // variable). |
| // |
| // If the two statements don't fit these criteria, return a pair of default- |
| // constructed values. |
| using ReturnTy = std::pair<const parser::ExecutionPartConstruct *, |
| const parser::ExecutionPartConstruct *>; |
| |
| SourcedActionStmt act1{GetActionStmt(ec1)}; |
| SourcedActionStmt act2{GetActionStmt(ec2)}; |
| auto maybeAssign1{GetEvaluateAssignment(act1.stmt)}; |
| auto maybeAssign2{GetEvaluateAssignment(act2.stmt)}; |
| if (!maybeAssign1 || !maybeAssign2) { |
| if (!IsAssignment(act1.stmt) || !IsAssignment(act2.stmt)) { |
| context_.Say(source, |
| "ATOMIC UPDATE operation with CAPTURE should contain two assignments"_err_en_US); |
| } |
| return std::make_pair(nullptr, nullptr); |
| } |
| |
| auto as1{*maybeAssign1}, as2{*maybeAssign2}; |
| |
| auto isUpdateCapture{ |
| [](const evaluate::Assignment &u, const evaluate::Assignment &c) { |
| return IsSameOrConvertOf(c.rhs, u.lhs); |
| }}; |
| |
| // Do some checks that narrow down the possible choices for the update |
| // and the capture statements. This will help to emit better diagnostics. |
| // 1. An assignment could be an update (cbu) if the left-hand side is a |
| // subexpression of the right-hand side. |
| // 2. An assignment could be a capture (cbc) if the right-hand side is |
| // a variable (or a function ref), with potential type conversions. |
| bool cbu1{IsVarSubexpressionOf(as1.lhs, as1.rhs)}; // Can as1 be an update? |
| bool cbu2{IsVarSubexpressionOf(as2.lhs, as2.rhs)}; // Can as2 be an update? |
| bool cbc1{IsVarOrFunctionRef(GetConvertInput(as1.rhs))}; // Can 1 be capture? |
| bool cbc2{IsVarOrFunctionRef(GetConvertInput(as2.rhs))}; // Can 2 be capture? |
| |
| // We want to diagnose cases where both assignments cannot be an update, |
| // or both cannot be a capture, as well as cases where either assignment |
| // cannot be any of these two. |
| // |
| // If we organize these boolean values into a matrix |
| // |cbu1 cbu2| |
| // |cbc1 cbc2| |
| // then we want to diagnose cases where the matrix has a zero (i.e. "false") |
| // row or column, including the case where everything is zero. All these |
| // cases correspond to the determinant of the matrix being 0, which suggests |
| // that checking the det may be a convenient diagnostic check. There is only |
| // one additional case where the det is 0, which is when the matrix is all 1 |
| // ("true"). The "all true" case represents the situation where both |
| // assignments could be an update as well as a capture. On the other hand, |
| // whenever det != 0, the roles of the update and the capture can be |
| // unambiguously assigned to as1 and as2 [1]. |
| // |
| // [1] This can be easily verified by hand: there are 10 2x2 matrices with |
| // det = 0, leaving 6 cases where det != 0: |
| // 0 1 0 1 1 0 1 0 1 1 1 1 |
| // 1 0 1 1 0 1 1 1 0 1 1 0 |
| // In each case the classification is unambiguous. |
| |
| // |cbu1 cbu2| |
| // det |cbc1 cbc2| = cbu1*cbc2 - cbu2*cbc1 |
| int det{int(cbu1) * int(cbc2) - int(cbu2) * int(cbc1)}; |
| |
| auto errorCaptureShouldRead{[&](const parser::CharBlock &source, |
| const std::string &expr) { |
| context_.Say(source, |
| "In ATOMIC UPDATE operation with CAPTURE the right-hand side of the capture assignment should read %s"_err_en_US, |
| expr); |
| }}; |
| |
| auto errorNeitherWorks{[&]() { |
| context_.Say(source, |
| "In ATOMIC UPDATE operation with CAPTURE neither statement could be the update or the capture"_err_en_US); |
| }}; |
| |
| auto makeSelectionFromDet{[&](int det) -> ReturnTy { |
| // If det != 0, then the checks unambiguously suggest a specific |
| // categorization. |
| // If det == 0, then this function should be called only if the |
| // checks haven't ruled out any possibility, i.e. when both assignments |
| // could still be either updates or captures. |
| if (det > 0) { |
| // as1 is update, as2 is capture |
| if (isUpdateCapture(as1, as2)) { |
| return std::make_pair(/*Update=*/ec1, /*Capture=*/ec2); |
| } else { |
| errorCaptureShouldRead(act2.source, as1.lhs.AsFortran()); |
| return std::make_pair(nullptr, nullptr); |
| } |
| } else if (det < 0) { |
| // as2 is update, as1 is capture |
| if (isUpdateCapture(as2, as1)) { |
| return std::make_pair(/*Update=*/ec2, /*Capture=*/ec1); |
| } else { |
| errorCaptureShouldRead(act1.source, as2.lhs.AsFortran()); |
| return std::make_pair(nullptr, nullptr); |
| } |
| } else { |
| bool updateFirst{isUpdateCapture(as1, as2)}; |
| bool captureFirst{isUpdateCapture(as2, as1)}; |
| if (updateFirst && captureFirst) { |
| // If both assignment could be the update and both could be the |
| // capture, emit a warning about the ambiguity. |
| context_.Say(act1.source, |
| "In ATOMIC UPDATE operation with CAPTURE either statement could be the update and the capture, assuming the first one is the capture statement"_warn_en_US); |
| return std::make_pair(/*Update=*/ec2, /*Capture=*/ec1); |
| } |
| if (updateFirst != captureFirst) { |
| const parser::ExecutionPartConstruct *upd{updateFirst ? ec1 : ec2}; |
| const parser::ExecutionPartConstruct *cap{captureFirst ? ec1 : ec2}; |
| return std::make_pair(upd, cap); |
| } |
| assert(!updateFirst && !captureFirst); |
| errorNeitherWorks(); |
| return std::make_pair(nullptr, nullptr); |
| } |
| }}; |
| |
| if (det != 0 || (cbu1 && cbu2 && cbc1 && cbc2)) { |
| return makeSelectionFromDet(det); |
| } |
| assert(det == 0 && "Prior checks should have covered det != 0"); |
| |
| // If neither of the statements is an RMW update, it could still be a |
| // "write" update. Pretty much any assignment can be a write update, so |
| // recompute det with cbu1 = cbu2 = true. |
| if (int writeDet{int(cbc2) - int(cbc1)}; writeDet || (cbc1 && cbc2)) { |
| return makeSelectionFromDet(writeDet); |
| } |
| |
| // It's only errors from here on. |
| |
| if (!cbu1 && !cbu2 && !cbc1 && !cbc2) { |
| errorNeitherWorks(); |
| return std::make_pair(nullptr, nullptr); |
| } |
| |
| // The remaining cases are that |
| // - no candidate for update, or for capture, |
| // - one of the assignments cannot be anything. |
| |
| if (!cbu1 && !cbu2) { |
| context_.Say(source, |
| "In ATOMIC UPDATE operation with CAPTURE neither statement could be the update"_err_en_US); |
| return std::make_pair(nullptr, nullptr); |
| } else if (!cbc1 && !cbc2) { |
| context_.Say(source, |
| "In ATOMIC UPDATE operation with CAPTURE neither statement could be the capture"_err_en_US); |
| return std::make_pair(nullptr, nullptr); |
| } |
| |
| if ((!cbu1 && !cbc1) || (!cbu2 && !cbc2)) { |
| auto &src = (!cbu1 && !cbc1) ? act1.source : act2.source; |
| context_.Say(src, |
| "In ATOMIC UPDATE operation with CAPTURE the statement could be neither the update nor the capture"_err_en_US); |
| return std::make_pair(nullptr, nullptr); |
| } |
| |
| // All cases should have been covered. |
| llvm_unreachable("Unchecked condition"); |
| } |
| |
| void OmpStructureChecker::CheckAtomicCaptureAssignment( |
| const evaluate::Assignment &capture, const SomeExpr &atom, |
| parser::CharBlock source) { |
| auto [lsrc, rsrc]{SplitAssignmentSource(source)}; |
| (void)lsrc; |
| const SomeExpr &cap{capture.lhs}; |
| |
| if (!IsVarOrFunctionRef(atom)) { |
| ErrorShouldBeVariable(atom, rsrc); |
| } else { |
| CheckAtomicVariable(atom, rsrc); |
| // This part should have been checked prior to calling this function. |
| assert(*GetConvertInput(capture.rhs) == atom && |
| "This cannot be a capture assignment"); |
| CheckStorageOverlap(atom, {cap}, source); |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicReadAssignment( |
| const evaluate::Assignment &read, parser::CharBlock source) { |
| auto [lsrc, rsrc]{SplitAssignmentSource(source)}; |
| (void)lsrc; |
| |
| if (auto maybe{GetConvertInput(read.rhs)}) { |
| const SomeExpr &atom{*maybe}; |
| |
| if (!IsVarOrFunctionRef(atom)) { |
| ErrorShouldBeVariable(atom, rsrc); |
| } else { |
| CheckAtomicVariable(atom, rsrc); |
| CheckStorageOverlap(atom, {read.lhs}, source); |
| } |
| } else { |
| ErrorShouldBeVariable(read.rhs, rsrc); |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicWriteAssignment( |
| const evaluate::Assignment &write, parser::CharBlock source) { |
| // [6.0:190:13-15] |
| // A write structured block is write-statement, a write statement that has |
| // one of the following forms: |
| // x = expr |
| // x => expr |
| auto [lsrc, rsrc]{SplitAssignmentSource(source)}; |
| const SomeExpr &atom{write.lhs}; |
| |
| if (!IsVarOrFunctionRef(atom)) { |
| ErrorShouldBeVariable(atom, rsrc); |
| } else { |
| CheckAtomicVariable(atom, lsrc); |
| CheckStorageOverlap(atom, {write.rhs}, source); |
| } |
| } |
| |
| std::optional<evaluate::Assignment> |
| OmpStructureChecker::CheckAtomicUpdateAssignment( |
| const evaluate::Assignment &update, parser::CharBlock source) { |
| // [6.0:191:1-7] |
| // An update structured block is update-statement, an update statement |
| // that has one of the following forms: |
| // x = x operator expr |
| // x = expr operator x |
| // x = intrinsic-procedure-name (x) |
| // x = intrinsic-procedure-name (x, expr-list) |
| // x = intrinsic-procedure-name (expr-list, x) |
| auto [lsrc, rsrc]{SplitAssignmentSource(source)}; |
| const SomeExpr &atom{update.lhs}; |
| |
| if (!IsVarOrFunctionRef(atom)) { |
| ErrorShouldBeVariable(atom, rsrc); |
| // Skip other checks. |
| return std::nullopt; |
| } |
| |
| CheckAtomicVariable(atom, lsrc); |
| |
| auto [hasErrors, tryReassoc]{CheckAtomicUpdateAssignmentRhs( |
| atom, update.rhs, source, /*suppressDiagnostics=*/true)}; |
| |
| if (!hasErrors) { |
| CheckStorageOverlap(atom, GetNonAtomArguments(atom, update.rhs), source); |
| return std::nullopt; |
| } else if (tryReassoc) { |
| ReassocRewriter ra(atom, context_); |
| SomeExpr raRhs{evaluate::rewrite::Mutator(ra)(update.rhs)}; |
| |
| std::tie(hasErrors, tryReassoc) = CheckAtomicUpdateAssignmentRhs( |
| atom, raRhs, source, /*suppressDiagnostics=*/true); |
| if (!hasErrors) { |
| CheckStorageOverlap(atom, GetNonAtomArguments(atom, raRhs), source); |
| |
| evaluate::Assignment raAssign(update); |
| raAssign.rhs = raRhs; |
| return raAssign; |
| } |
| } |
| |
| // This is guaranteed to report errors. |
| CheckAtomicUpdateAssignmentRhs( |
| atom, update.rhs, source, /*suppressDiagnostics=*/false); |
| return std::nullopt; |
| } |
| |
| std::pair<bool, bool> OmpStructureChecker::CheckAtomicUpdateAssignmentRhs( |
| const SomeExpr &atom, const SomeExpr &rhs, parser::CharBlock source, |
| bool suppressDiagnostics) { |
| auto [lsrc, rsrc]{SplitAssignmentSource(source)}; |
| (void)lsrc; |
| |
| std::pair<operation::Operator, std::vector<SomeExpr>> top{ |
| operation::Operator::Unknown, {}}; |
| if (auto &&maybeInput{GetConvertInput(rhs)}) { |
| top = GetTopLevelOperationIgnoreResizing(*maybeInput); |
| } |
| switch (top.first) { |
| case operation::Operator::Add: |
| case operation::Operator::Sub: |
| case operation::Operator::Mul: |
| case operation::Operator::Div: |
| case operation::Operator::And: |
| case operation::Operator::Or: |
| case operation::Operator::Eqv: |
| case operation::Operator::Neqv: |
| case operation::Operator::Min: |
| case operation::Operator::Max: |
| case operation::Operator::Identity: |
| break; |
| case operation::Operator::Call: |
| if (!suppressDiagnostics) { |
| context_.Say(source, |
| "A call to this function is not a valid ATOMIC UPDATE operation"_err_en_US); |
| } |
| return std::make_pair(true, false); |
| case operation::Operator::Convert: |
| if (!suppressDiagnostics) { |
| context_.Say(source, |
| "An implicit or explicit type conversion is not a valid ATOMIC UPDATE operation"_err_en_US); |
| } |
| return std::make_pair(true, false); |
| case operation::Operator::Intrinsic: |
| if (!suppressDiagnostics) { |
| context_.Say(source, |
| "This intrinsic function is not a valid ATOMIC UPDATE operation"_err_en_US); |
| } |
| return std::make_pair(true, false); |
| case operation::Operator::Constant: |
| case operation::Operator::Unknown: |
| if (!suppressDiagnostics) { |
| context_.Say( |
| source, "This is not a valid ATOMIC UPDATE operation"_err_en_US); |
| } |
| return std::make_pair(true, false); |
| default: |
| assert( |
| top.first != operation::Operator::Identity && "Handle this separately"); |
| if (!suppressDiagnostics) { |
| context_.Say(source, |
| "The %s operator is not a valid ATOMIC UPDATE operation"_err_en_US, |
| operation::ToString(top.first)); |
| } |
| return std::make_pair(true, false); |
| } |
| // Check how many times `atom` occurs as an argument, if it's a subexpression |
| // of an argument, and collect the non-atom arguments. |
| std::vector<SomeExpr> nonAtom; |
| MaybeExpr subExpr; |
| auto atomCount{[&]() { |
| int count{0}; |
| for (const SomeExpr &arg : top.second) { |
| if (IsSameOrConvertOf(arg, atom)) { |
| ++count; |
| } else { |
| if (!subExpr && evaluate::IsVarSubexpressionOf(atom, arg)) { |
| subExpr = arg; |
| } |
| nonAtom.push_back(arg); |
| } |
| } |
| return count; |
| }()}; |
| |
| bool hasError{false}, tryReassoc{false}; |
| if (subExpr) { |
| if (!suppressDiagnostics) { |
| context_.Say(rsrc, |
| "The atomic variable %s cannot be a proper subexpression of an argument (here: %s) in the update operation"_err_en_US, |
| atom.AsFortran(), subExpr->AsFortran()); |
| } |
| hasError = true; |
| } |
| if (top.first == operation::Operator::Identity) { |
| // This is "x = y". |
| assert((atomCount == 0 || atomCount == 1) && "Unexpected count"); |
| if (atomCount == 0) { |
| if (!suppressDiagnostics) { |
| context_.Say(rsrc, |
| "The atomic variable %s should appear as an argument in the update operation"_err_en_US, |
| atom.AsFortran()); |
| } |
| hasError = true; |
| } |
| } else { |
| if (atomCount == 0) { |
| if (!suppressDiagnostics) { |
| context_.Say(rsrc, |
| "The atomic variable %s should appear as an argument of the top-level %s operator"_err_en_US, |
| atom.AsFortran(), operation::ToString(top.first)); |
| } |
| // If `atom` is a proper subexpression, and it not present as an |
| // argument on its own, reassociation may be able to help. |
| tryReassoc = subExpr.has_value(); |
| hasError = true; |
| } else if (atomCount > 1) { |
| if (!suppressDiagnostics) { |
| context_.Say(rsrc, |
| "The atomic variable %s should be exactly one of the arguments of the top-level %s operator"_err_en_US, |
| atom.AsFortran(), operation::ToString(top.first)); |
| } |
| hasError = true; |
| } |
| } |
| |
| return std::make_pair(hasError, tryReassoc); |
| } |
| |
| void OmpStructureChecker::CheckAtomicConditionalUpdateAssignment( |
| const SomeExpr &cond, parser::CharBlock condSource, |
| const evaluate::Assignment &assign, parser::CharBlock assignSource) { |
| auto [alsrc, arsrc]{SplitAssignmentSource(assignSource)}; |
| const SomeExpr &atom{assign.lhs}; |
| |
| if (!IsVarOrFunctionRef(atom)) { |
| ErrorShouldBeVariable(atom, arsrc); |
| // Skip other checks. |
| return; |
| } |
| |
| CheckAtomicVariable(atom, alsrc); |
| |
| auto top{GetTopLevelOperationIgnoreResizing(cond)}; |
| // Missing arguments to operations would have been diagnosed by now. |
| |
| switch (top.first) { |
| case operation::Operator::Associated: |
| if (atom != top.second.front()) { |
| context_.Say(assignSource, |
| "The pointer argument to ASSOCIATED must be same as the target of the assignment"_err_en_US); |
| } |
| break; |
| // x equalop e | e equalop x (allowing "e equalop x" is an extension) |
| case operation::Operator::Eq: |
| case operation::Operator::Eqv: |
| // x ordop expr | expr ordop x |
| case operation::Operator::Lt: |
| case operation::Operator::Gt: { |
| const SomeExpr &arg0{top.second[0]}; |
| const SomeExpr &arg1{top.second[1]}; |
| if (IsSameOrConvertOf(arg0, atom)) { |
| CheckStorageOverlap(atom, {arg1}, condSource); |
| } else if (IsSameOrConvertOf(arg1, atom)) { |
| CheckStorageOverlap(atom, {arg0}, condSource); |
| } else { |
| assert(top.first != operation::Operator::Identity && |
| "Handle this separately"); |
| context_.Say(assignSource, |
| "An argument of the %s operator should be the target of the assignment"_err_en_US, |
| operation::ToString(top.first)); |
| } |
| break; |
| } |
| case operation::Operator::Identity: |
| case operation::Operator::True: |
| case operation::Operator::False: |
| break; |
| default: |
| assert( |
| top.first != operation::Operator::Identity && "Handle this separately"); |
| context_.Say(condSource, |
| "The %s operator is not a valid condition for ATOMIC operation"_err_en_US, |
| operation::ToString(top.first)); |
| break; |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicConditionalUpdateStmt( |
| const AnalyzedCondStmt &update, parser::CharBlock source) { |
| // The condition/statements must be: |
| // - cond: x equalop e ift: x = d iff: - |
| // - cond: x ordop expr ift: x = expr iff: - (+ commute ordop) |
| // - cond: associated(x) ift: x => expr iff: - |
| // - cond: associated(x, e) ift: x => expr iff: - |
| |
| // The if-true statement must be present, and must be an assignment. |
| auto maybeAssign{GetEvaluateAssignment(update.ift.stmt)}; |
| if (!maybeAssign) { |
| if (update.ift.stmt && !IsAssignment(update.ift.stmt)) { |
| context_.Say(update.ift.source, |
| "In ATOMIC UPDATE COMPARE the update statement should be an assignment"_err_en_US); |
| } else { |
| context_.Say( |
| source, "Invalid body of ATOMIC UPDATE COMPARE operation"_err_en_US); |
| } |
| return; |
| } |
| const evaluate::Assignment assign{*maybeAssign}; |
| const SomeExpr &atom{assign.lhs}; |
| |
| CheckAtomicConditionalUpdateAssignment( |
| update.cond, update.source, assign, update.ift.source); |
| |
| CheckStorageOverlap(atom, {assign.rhs}, update.ift.source); |
| |
| if (update.iff) { |
| context_.Say(update.iff.source, |
| "In ATOMIC UPDATE COMPARE the update statement should not have an ELSE branch"_err_en_US); |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicUpdateOnly( |
| const parser::OpenMPAtomicConstruct &x, const parser::Block &body, |
| parser::CharBlock source) { |
| if (body.size() == 1) { |
| SourcedActionStmt action{GetActionStmt(&body.front())}; |
| if (auto maybeUpdate{GetEvaluateAssignment(action.stmt)}) { |
| const SomeExpr &atom{maybeUpdate->lhs}; |
| auto maybeAssign{ |
| CheckAtomicUpdateAssignment(*maybeUpdate, action.source)}; |
| auto &updateAssign{maybeAssign.has_value() ? maybeAssign : maybeUpdate}; |
| |
| using Analysis = parser::OpenMPAtomicConstruct::Analysis; |
| x.analysis = AtomicAnalysis(atom) |
| .addOp0(Analysis::Update, updateAssign) |
| .addOp1(Analysis::None); |
| } else if (!IsAssignment(action.stmt)) { |
| context_.Say( |
| source, "ATOMIC UPDATE operation should be an assignment"_err_en_US); |
| } |
| } else { |
| context_.Say(x.source, |
| "ATOMIC UPDATE operation should have a single statement"_err_en_US); |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicConditionalUpdate( |
| const parser::OpenMPAtomicConstruct &x, const parser::Block &body, |
| parser::CharBlock source) { |
| // Allowable forms are (single-statement): |
| // - if ... |
| // - x = (... ? ... : x) |
| // and two-statement: |
| // - r = cond ; if (r) ... |
| |
| const parser::ExecutionPartConstruct *ust{nullptr}; // update |
| const parser::ExecutionPartConstruct *cst{nullptr}; // condition |
| |
| if (body.size() == 1) { |
| ust = &body.front(); |
| } else if (body.size() == 2) { |
| cst = &body.front(); |
| ust = &body.back(); |
| } else { |
| context_.Say(source, |
| "ATOMIC UPDATE COMPARE operation should contain one or two statements"_err_en_US); |
| return; |
| } |
| |
| // Flang doesn't support conditional-expr yet, so all update statements |
| // are if-statements. |
| |
| // IfStmt: if (...) ... |
| // IfConstruct: if (...) then ... endif |
| auto maybeUpdate{AnalyzeConditionalStmt(ust)}; |
| if (!maybeUpdate) { |
| context_.Say(source, |
| "In ATOMIC UPDATE COMPARE the update statement should be a conditional statement"_err_en_US); |
| return; |
| } |
| |
| AnalyzedCondStmt &update{*maybeUpdate}; |
| |
| if (SourcedActionStmt action{GetActionStmt(cst)}) { |
| // The "condition" statement must be `r = cond`. |
| if (auto maybeCond{GetEvaluateAssignment(action.stmt)}) { |
| if (maybeCond->lhs != update.cond) { |
| context_.Say(update.source, |
| "In ATOMIC UPDATE COMPARE the conditional statement must use %s as the condition"_err_en_US, |
| maybeCond->lhs.AsFortran()); |
| } else { |
| // If it's "r = ...; if (r) ..." then put the original condition |
| // in `update`. |
| update.cond = maybeCond->rhs; |
| } |
| } else { |
| context_.Say(action.source, |
| "In ATOMIC UPDATE COMPARE with two statements the first statement should compute the condition"_err_en_US); |
| } |
| } |
| |
| evaluate::Assignment assign{*GetEvaluateAssignment(update.ift.stmt)}; |
| |
| CheckAtomicConditionalUpdateStmt(update, source); |
| if (IsCheckForAssociated(update.cond)) { |
| if (!IsPointerAssignment(assign)) { |
| context_.Say(source, |
| "The assignment should be a pointer-assignment when the condition is ASSOCIATED"_err_en_US); |
| } |
| } else { |
| if (IsPointerAssignment(assign)) { |
| context_.Say(source, |
| "The assignment cannot be a pointer-assignment except when the condition is ASSOCIATED"_err_en_US); |
| } |
| } |
| |
| using Analysis = parser::OpenMPAtomicConstruct::Analysis; |
| const SomeExpr &atom{assign.lhs}; |
| |
| x.analysis = AtomicAnalysis(atom, update.cond) |
| .addOp0(Analysis::Update | Analysis::IfTrue, assign) |
| .addOp1(Analysis::None); |
| } |
| |
| void OmpStructureChecker::CheckAtomicUpdateCapture( |
| const parser::OpenMPAtomicConstruct &x, const parser::Block &body, |
| parser::CharBlock source) { |
| if (body.size() != 2) { |
| context_.Say(source, |
| "ATOMIC UPDATE operation with CAPTURE should contain two statements"_err_en_US); |
| return; |
| } |
| |
| auto [uec, cec]{CheckUpdateCapture(&body.front(), &body.back(), source)}; |
| if (!uec || !cec) { |
| // Diagnostics already emitted. |
| return; |
| } |
| SourcedActionStmt uact{GetActionStmt(uec)}; |
| SourcedActionStmt cact{GetActionStmt(cec)}; |
| // The "dereferences" of std::optional are guaranteed to be valid after |
| // CheckUpdateCapture. |
| evaluate::Assignment update{*GetEvaluateAssignment(uact.stmt)}; |
| evaluate::Assignment capture{*GetEvaluateAssignment(cact.stmt)}; |
| |
| const SomeExpr &atom{update.lhs}; |
| |
| using Analysis = parser::OpenMPAtomicConstruct::Analysis; |
| int action; |
| |
| std::optional<evaluate::Assignment> updateAssign{update}; |
| if (IsMaybeAtomicWrite(update)) { |
| action = Analysis::Write; |
| CheckAtomicWriteAssignment(update, uact.source); |
| } else { |
| action = Analysis::Update; |
| if (auto &&maybe{CheckAtomicUpdateAssignment(update, uact.source)}) { |
| updateAssign = maybe; |
| } |
| } |
| CheckAtomicCaptureAssignment(capture, atom, cact.source); |
| |
| if (IsPointerAssignment(*updateAssign) != IsPointerAssignment(capture)) { |
| context_.Say(cact.source, |
| "The update and capture assignments should both be pointer-assignments or both be non-pointer-assignments"_err_en_US); |
| return; |
| } |
| |
| if (GetActionStmt(&body.front()).stmt == uact.stmt) { |
| x.analysis = AtomicAnalysis(atom) |
| .addOp0(action, updateAssign) |
| .addOp1(Analysis::Read, capture); |
| } else { |
| x.analysis = AtomicAnalysis(atom) |
| .addOp0(Analysis::Read, capture) |
| .addOp1(action, updateAssign); |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicConditionalUpdateCapture( |
| const parser::OpenMPAtomicConstruct &x, const parser::Block &body, |
| parser::CharBlock source) { |
| // There are two different variants of this: |
| // (1) conditional-update and capture separately: |
| // This form only allows single-statement updates, i.e. the update |
| // form "r = cond; if (r) ..." is not allowed. |
| // (2) conditional-update combined with capture in a single statement: |
| // This form does allow the condition to be calculated separately, |
| // i.e. "r = cond; if (r) ...". |
| // Regardless of what form it is, the actual update assignment is a |
| // proper write, i.e. "x = d", where d does not depend on x. |
| |
| AnalyzedCondStmt update; |
| SourcedActionStmt capture; |
| bool captureAlways{true}, captureFirst{true}; |
| |
| auto extractCapture{[&]() { |
| capture = update.iff; |
| captureAlways = false; |
| update.iff = SourcedActionStmt{}; |
| }}; |
| |
| auto classifyNonUpdate{[&](const SourcedActionStmt &action) { |
| // The non-update statement is either "r = cond" or the capture. |
| if (auto maybeAssign{GetEvaluateAssignment(action.stmt)}) { |
| if (update.cond == maybeAssign->lhs) { |
| // If this is "r = cond; if (r) ...", then update the condition. |
| update.cond = maybeAssign->rhs; |
| update.source = action.source; |
| // In this form, the update and the capture are combined into |
| // an IF-THEN-ELSE statement. |
| extractCapture(); |
| } else { |
| // Assume this is the capture-statement. |
| capture = action; |
| } |
| } |
| }}; |
| |
| if (body.size() == 2) { |
| // This could be |
| // - capture; conditional-update (in any order), or |
| // - r = cond; if (r) capture-update |
| const parser::ExecutionPartConstruct *st1{&body.front()}; |
| const parser::ExecutionPartConstruct *st2{&body.back()}; |
| // In either case, the conditional statement can be analyzed by |
| // AnalyzeConditionalStmt, whereas the other statement cannot. |
| if (auto maybeUpdate1{AnalyzeConditionalStmt(st1)}) { |
| update = *maybeUpdate1; |
| classifyNonUpdate(GetActionStmt(st2)); |
| captureFirst = false; |
| } else if (auto maybeUpdate2{AnalyzeConditionalStmt(st2)}) { |
| update = *maybeUpdate2; |
| classifyNonUpdate(GetActionStmt(st1)); |
| } else { |
| // None of the statements are conditional, this rules out the |
| // "r = cond; if (r) ..." and the "capture + conditional-update" |
| // variants. This could still be capture + write (which is classified |
| // as conditional-update-capture in the spec). |
| auto [uec, cec]{CheckUpdateCapture(st1, st2, source)}; |
| if (!uec || !cec) { |
| // Diagnostics already emitted. |
| return; |
| } |
| SourcedActionStmt uact{GetActionStmt(uec)}; |
| SourcedActionStmt cact{GetActionStmt(cec)}; |
| update.ift = uact; |
| capture = cact; |
| if (uec == st1) { |
| captureFirst = false; |
| } |
| } |
| } else if (body.size() == 1) { |
| if (auto maybeUpdate{AnalyzeConditionalStmt(&body.front())}) { |
| update = *maybeUpdate; |
| // This is the form with update and capture combined into an IF-THEN-ELSE |
| // statement. The capture-statement is always the ELSE branch. |
| extractCapture(); |
| } else { |
| goto invalid; |
| } |
| } else { |
| context_.Say(source, |
| "ATOMIC UPDATE COMPARE CAPTURE operation should contain one or two statements"_err_en_US); |
| return; |
| invalid: |
| context_.Say(source, |
| "Invalid body of ATOMIC UPDATE COMPARE CAPTURE operation"_err_en_US); |
| return; |
| } |
| |
| // The update must have a form `x = d` or `x => d`. |
| if (auto maybeWrite{GetEvaluateAssignment(update.ift.stmt)}) { |
| const SomeExpr &atom{maybeWrite->lhs}; |
| CheckAtomicWriteAssignment(*maybeWrite, update.ift.source); |
| if (auto maybeCapture{GetEvaluateAssignment(capture.stmt)}) { |
| CheckAtomicCaptureAssignment(*maybeCapture, atom, capture.source); |
| |
| if (IsPointerAssignment(*maybeWrite) != |
| IsPointerAssignment(*maybeCapture)) { |
| context_.Say(capture.source, |
| "The update and capture assignments should both be pointer-assignments or both be non-pointer-assignments"_err_en_US); |
| return; |
| } |
| } else { |
| if (!IsAssignment(capture.stmt)) { |
| context_.Say(capture.source, |
| "In ATOMIC UPDATE COMPARE CAPTURE the capture statement should be an assignment"_err_en_US); |
| } |
| return; |
| } |
| } else { |
| if (!IsAssignment(update.ift.stmt)) { |
| context_.Say(update.ift.source, |
| "In ATOMIC UPDATE COMPARE CAPTURE the update statement should be an assignment"_err_en_US); |
| } |
| return; |
| } |
| |
| // update.iff should be empty here, the capture statement should be |
| // stored in "capture". |
| |
| // Fill out the analysis in the AST node. |
| using Analysis = parser::OpenMPAtomicConstruct::Analysis; |
| bool condUnused{std::visit( |
| [](auto &&s) { |
| using BareS = llvm::remove_cvref_t<decltype(s)>; |
| if constexpr (std::is_same_v<BareS, evaluate::NullPointer>) { |
| return true; |
| } else { |
| return false; |
| } |
| }, |
| update.cond.u)}; |
| |
| int updateWhen{!condUnused ? Analysis::IfTrue : 0}; |
| int captureWhen{!captureAlways ? Analysis::IfFalse : 0}; |
| |
| evaluate::Assignment updAssign{*GetEvaluateAssignment(update.ift.stmt)}; |
| evaluate::Assignment capAssign{*GetEvaluateAssignment(capture.stmt)}; |
| const SomeExpr &atom{updAssign.lhs}; |
| |
| if (captureFirst) { |
| x.analysis = AtomicAnalysis(atom, update.cond) |
| .addOp0(Analysis::Read | captureWhen, capAssign) |
| .addOp1(Analysis::Write | updateWhen, updAssign); |
| } else { |
| x.analysis = AtomicAnalysis(atom, update.cond) |
| .addOp0(Analysis::Write | updateWhen, updAssign) |
| .addOp1(Analysis::Read | captureWhen, capAssign); |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicRead( |
| const parser::OpenMPAtomicConstruct &x) { |
| // [6.0:190:5-7] |
| // A read structured block is read-statement, a read statement that has one |
| // of the following forms: |
| // v = x |
| // v => x |
| auto &block{std::get<parser::Block>(x.t)}; |
| |
| // Read cannot be conditional or have a capture statement. |
| if (x.IsCompare() || x.IsCapture()) { |
| context_.Say(x.BeginDir().source, |
| "ATOMIC READ cannot have COMPARE or CAPTURE clauses"_err_en_US); |
| return; |
| } |
| |
| const parser::Block &body{GetInnermostExecPart(block)}; |
| |
| if (body.size() == 1) { |
| SourcedActionStmt action{GetActionStmt(&body.front())}; |
| if (auto maybeRead{GetEvaluateAssignment(action.stmt)}) { |
| CheckAtomicReadAssignment(*maybeRead, action.source); |
| |
| if (auto maybe{GetConvertInput(maybeRead->rhs)}) { |
| const SomeExpr &atom{*maybe}; |
| using Analysis = parser::OpenMPAtomicConstruct::Analysis; |
| x.analysis = AtomicAnalysis(atom) |
| .addOp0(Analysis::Read, maybeRead) |
| .addOp1(Analysis::None); |
| } |
| } else if (!IsAssignment(action.stmt)) { |
| context_.Say( |
| x.source, "ATOMIC READ operation should be an assignment"_err_en_US); |
| } |
| } else { |
| context_.Say(x.source, |
| "ATOMIC READ operation should have a single statement"_err_en_US); |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicWrite( |
| const parser::OpenMPAtomicConstruct &x) { |
| auto &block{std::get<parser::Block>(x.t)}; |
| |
| // Write cannot be conditional or have a capture statement. |
| if (x.IsCompare() || x.IsCapture()) { |
| context_.Say(x.BeginDir().source, |
| "ATOMIC WRITE cannot have COMPARE or CAPTURE clauses"_err_en_US); |
| return; |
| } |
| |
| const parser::Block &body{GetInnermostExecPart(block)}; |
| |
| if (body.size() == 1) { |
| SourcedActionStmt action{GetActionStmt(&body.front())}; |
| if (auto maybeWrite{GetEvaluateAssignment(action.stmt)}) { |
| const SomeExpr &atom{maybeWrite->lhs}; |
| CheckAtomicWriteAssignment(*maybeWrite, action.source); |
| |
| using Analysis = parser::OpenMPAtomicConstruct::Analysis; |
| x.analysis = AtomicAnalysis(atom) |
| .addOp0(Analysis::Write, maybeWrite) |
| .addOp1(Analysis::None); |
| } else if (!IsAssignment(action.stmt)) { |
| context_.Say( |
| x.source, "ATOMIC WRITE operation should be an assignment"_err_en_US); |
| } |
| } else { |
| context_.Say(x.source, |
| "ATOMIC WRITE operation should have a single statement"_err_en_US); |
| } |
| } |
| |
| void OmpStructureChecker::CheckAtomicUpdate( |
| const parser::OpenMPAtomicConstruct &x) { |
| auto &block{std::get<parser::Block>(x.t)}; |
| |
| bool isConditional{x.IsCompare()}; |
| bool isCapture{x.IsCapture()}; |
| const parser::Block &body{GetInnermostExecPart(block)}; |
| |
| if (isConditional && isCapture) { |
| CheckAtomicConditionalUpdateCapture(x, body, x.source); |
| } else if (isConditional) { |
| CheckAtomicConditionalUpdate(x, body, x.source); |
| } else if (isCapture) { |
| CheckAtomicUpdateCapture(x, body, x.source); |
| } else { // update-only |
| CheckAtomicUpdateOnly(x, body, x.source); |
| } |
| } |
| |
| void OmpStructureChecker::Enter(const parser::OpenMPAtomicConstruct &x) { |
| if (visitedAtomicSource_.empty()) |
| visitedAtomicSource_ = x.source; |
| |
| // All of the following groups have the "exclusive" property, i.e. at |
| // most one clause from each group is allowed. |
| // The exclusivity-checking code should eventually be unified for all |
| // clauses, with clause groups defined in OMP.td. |
| std::array atomic{llvm::omp::Clause::OMPC_read, |
| llvm::omp::Clause::OMPC_update, llvm::omp::Clause::OMPC_write}; |
| std::array memoryOrder{llvm::omp::Clause::OMPC_acq_rel, |
| llvm::omp::Clause::OMPC_acquire, llvm::omp::Clause::OMPC_relaxed, |
| llvm::omp::Clause::OMPC_release, llvm::omp::Clause::OMPC_seq_cst}; |
| |
| auto checkExclusive{[&](llvm::ArrayRef<llvm::omp::Clause> group, |
| std::string_view name, |
| const parser::OmpClauseList &clauses) { |
| const parser::OmpClause *present{nullptr}; |
| for (const parser::OmpClause &clause : clauses.v) { |
| llvm::omp::Clause id{clause.Id()}; |
| if (!llvm::is_contained(group, id)) { |
| continue; |
| } |
| if (present == nullptr) { |
| present = &clause; |
| continue; |
| } else if (id == present->Id()) { |
| // Ignore repetitions of the same clause, those will be diagnosed |
| // separately. |
| continue; |
| } |
| parser::MessageFormattedText txt( |
| "At most one clause from the '%s' group is allowed on ATOMIC construct"_err_en_US, |
| name.data()); |
| parser::Message message(clause.source, txt); |
| message.Attach(present->source, |
| "Previous clause from this group provided here"_en_US); |
| context_.Say(std::move(message)); |
| return; |
| } |
| }}; |
| |
| const parser::OmpDirectiveSpecification &dirSpec{x.BeginDir()}; |
| auto &dir{std::get<parser::OmpDirectiveName>(dirSpec.t)}; |
| PushContextAndClauseSets(dir.source, llvm::omp::Directive::OMPD_atomic); |
| llvm::omp::Clause kind{x.GetKind()}; |
| |
| checkExclusive(atomic, "atomic", dirSpec.Clauses()); |
| checkExclusive(memoryOrder, "memory-order", dirSpec.Clauses()); |
| |
| switch (kind) { |
| case llvm::omp::Clause::OMPC_read: |
| CheckAtomicRead(x); |
| break; |
| case llvm::omp::Clause::OMPC_write: |
| CheckAtomicWrite(x); |
| break; |
| case llvm::omp::Clause::OMPC_update: |
| CheckAtomicUpdate(x); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| void OmpStructureChecker::Leave(const parser::OpenMPAtomicConstruct &) { |
| dirContext_.pop_back(); |
| } |
| |
| // Rewrite min/max: |
| // Min and max intrinsics in Fortran take an arbitrary number of arguments |
| // (two or more). The first two are mandatory, the rest is optional. That |
| // means that arguments beyond the first two may be optional dummy argument |
| // from the caller. In that case, a reference to such an argument will |
| // cause presence test to be emitted, which cannot go inside of the atomic |
| // operation. Since the atom operand must be present, rewrite the min/max |
| // operation in a way that avoid the presence tests in the atomic code. |
| // For example, in |
| // subroutine f(atom, x, y, z) |
| // integer :: atom, x |
| // integer, optional :: y, z |
| // !$omp atomic update |
| // atom = min(atom, x, y, z) |
| // end |
| // the min operation will become |
| // atom = min(atom, min(x, y, z)) |
| // and in the final code |
| // // Presence check is fine here. |
| // tmp = min(x, y, z) |
| // atomic update { |
| // // Both operands are mandatory, no presence check needed. |
| // atom = min(atom, tmp) |
| // } |
| struct MinMaxRewriter : public evaluate::rewrite::Identity { |
| using Id = evaluate::rewrite::Identity; |
| using Id::operator(); |
| |
| MinMaxRewriter(const SomeExpr &atom) : atom_(atom) {} |
| |
| static bool IsMinMax(const evaluate::ProcedureDesignator &p) { |
| if (auto *intrin{p.GetSpecificIntrinsic()}) { |
| return intrin->name == "min" || intrin->name == "max"; |
| } |
| return false; |
| } |
| |
| // Take a list of arguments to a min/max operation, e.g. [a0, a1, ...] |
| // One of the a_i's, say a_t, must be the atom. |
| // Generate |
| // min/max(a_t, min/max(a0, a1, ... [except a_t])) |
| template <typename T> |
| evaluate::Expr<T> operator()( |
| evaluate::Expr<T> &&x, const evaluate::FunctionRef<T> &f) { |
| const evaluate::ProcedureDesignator &proc = f.proc(); |
| if (!IsMinMax(proc) || f.arguments().size() <= 2) { |
| return Id::operator()(std::move(x), f); |
| } |
| |
| // Collect arguments as SomeExpr's and find out which argument |
| // corresponds to atom. |
| const SomeExpr *atomArg{nullptr}; |
| std::vector<const SomeExpr *> args; |
| for (const std::optional<evaluate::ActualArgument> &a : f.arguments()) { |
| if (!a) { |
| continue; |
| } |
| if (const SomeExpr *e{a->UnwrapExpr()}) { |
| if (evaluate::IsSameOrConvertOf(*e, atom_)) { |
| atomArg = e; |
| } |
| args.push_back(e); |
| } |
| } |
| if (!atomArg) { |
| return Id::operator()(std::move(x), f); |
| } |
| |
| evaluate::ActualArguments nonAtoms; |
| |
| auto AsActual = [](const SomeExpr &z) { |
| SomeExpr copy = z; |
| return evaluate::ActualArgument(std::move(copy)); |
| }; |
| // Semantic checks guarantee that the "atom" shows exactly once in the |
| // argument list (with potential conversions around it). |
| // For the first two (non-optional) arguments, if "atom" is among them, |
| // replace it with another occurrence of the other non-optional argument. |
| if (atomArg == args[0]) { |
| // (atom, x, y...) -> (x, x, y...) |
| nonAtoms.push_back(AsActual(*args[1])); |
| nonAtoms.push_back(AsActual(*args[1])); |
| } else if (atomArg == args[1]) { |
| // (x, atom, y...) -> (x, x, y...) |
| nonAtoms.push_back(AsActual(*args[0])); |
| nonAtoms.push_back(AsActual(*args[0])); |
| } else { |
| // (x, y, z...) -> unchanged |
| nonAtoms.push_back(AsActual(*args[0])); |
| nonAtoms.push_back(AsActual(*args[1])); |
| } |
| |
| // The rest of arguments are optional, so we can just skip "atom". |
| for (size_t i = 2, e = args.size(); i != e; ++i) { |
| if (atomArg != args[i]) |
| nonAtoms.push_back(AsActual(*args[i])); |
| } |
| |
| SomeExpr tmp = evaluate::AsGenericExpr( |
| evaluate::FunctionRef<T>(AsRvalue(proc), AsRvalue(nonAtoms))); |
| |
| return evaluate::Expr<T>(evaluate::FunctionRef<T>( |
| AsRvalue(proc), {AsActual(*atomArg), AsActual(tmp)})); |
| } |
| |
| private: |
| const SomeExpr &atom_; |
| }; |
| |
| static MaybeExpr PostSemaRewrite(const SomeExpr &atom, const SomeExpr &expr) { |
| MinMaxRewriter rewriter(atom); |
| return evaluate::rewrite::Mutator(rewriter)(expr); |
| } |
| |
| } // namespace Fortran::semantics |