| //===-- 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/openmp-utils.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::parser::omp; | |
| 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::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>>> | |
| auto &expr{parser::UnwrapRef<parser::Expr>(logical)}; | |
| 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::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, bool checkTypeOnPointer) { | |
| 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; | |
| } | |
| // Apply pointer-to-non-intrinsic rule only for intrinsic-assignment paths. | |
| if (checkTypeOnPointer) { | |
| using Category = DeclTypeSpec::Category; | |
| Category cat{typeSpec->category()}; | |
| if (cat != Category::Numeric && cat != Category::Logical) { | |
| std::string details = " has the POINTER attribute"; | |
| if (const auto *derived{typeSpec->AsDerived()}) { | |
| details += " and derived type '"s + derived->name().ToString() + "'"; | |
| } | |
| context_.Say(source, | |
| "ATOMIC operation requires an intrinsic scalar variable; '%s'%s"_err_en_US, | |
| sym->name(), details); | |
| 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, bool checkTypeOnPointer) { | |
| if (atom.Rank() != 0) { | |
| context_.Say(source, "Atomic variable %s should be a scalar"_err_en_US, | |
| atom.AsFortran()); | |
| } | |
| std::vector<SomeExpr> dsgs{GetTopLevelDesignators(atom)}; | |
| // Procedure references are valid if they return a pointer to a scalar. | |
| // Just return if we don't have exactly one designator - other checks will | |
| // diagnose any actual errors. | |
| if (dsgs.size() != 1) { | |
| return; | |
| } | |
| SymbolVector syms{evaluate::GetSymbolVector(dsgs.front())}; | |
| if (syms.empty()) { | |
| return; | |
| } | |
| CheckAtomicType(syms.back(), source, atom.AsFortran(), checkTypeOnPointer); | |
| if (!IsArrayElement(atom) && !ExtractComplexPart(atom)) { | |
| if (IsAllocatable(syms.back())) { | |
| 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, /*checkTypeOnPointer=*/!IsPointerAssignment(capture)); | |
| // 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, /*checkTypeOnPointer=*/!IsPointerAssignment(read)); | |
| 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, /*checkTypeOnPointer=*/!IsPointerAssignment(write)); | |
| 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, /*checkTypeOnPointer=*/!IsPointerAssignment(update)); | |
| 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, /*checkTypeOnPointer=*/!IsPointerAssignment(assign)); | |
| 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 |