blob: 581236cc725474f79a3ab3ebd0a74c305d55721f [file] [edit]
//===-- lib/Semantics/check-cuda.cpp ----------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "check-cuda.h"
#include "flang/Common/restorer.h"
#include "flang/Common/template.h"
#include "flang/Evaluate/fold.h"
#include "flang/Evaluate/tools.h"
#include "flang/Evaluate/traverse.h"
#include "flang/Parser/parse-tree-visitor.h"
#include "flang/Parser/parse-tree.h"
#include "flang/Parser/tools.h"
#include "flang/Semantics/expression.h"
#include "flang/Semantics/symbol.h"
#include "flang/Semantics/tools.h"
#include "llvm/ADT/StringSet.h"
// Once labeled DO constructs have been canonicalized and their parse subtrees
// transformed into parser::DoConstructs, scan the parser::Blocks of the program
// and merge adjacent CUFKernelDoConstructs and DoConstructs whenever the
// CUFKernelDoConstruct doesn't already have an embedded DoConstruct. Also
// emit errors about improper or missing DoConstructs.
namespace Fortran::parser {
struct Mutator {
template <typename A> bool Pre(A &) { return true; }
template <typename A> void Post(A &) {}
bool Pre(Block &);
};
bool Mutator::Pre(Block &block) {
for (auto iter{block.begin()}; iter != block.end(); ++iter) {
if (auto *kernel{Unwrap<CUFKernelDoConstruct>(*iter)}) {
auto &nested{std::get<std::optional<DoConstruct>>(kernel->t)};
if (!nested) {
if (auto next{iter}; ++next != block.end()) {
if (auto *doConstruct{Unwrap<DoConstruct>(*next)}) {
nested = std::move(*doConstruct);
block.erase(next);
}
}
}
} else {
Walk(*iter, *this);
}
}
return false;
}
} // namespace Fortran::parser
namespace Fortran::semantics {
bool CanonicalizeCUDA(parser::Program &program) {
parser::Mutator mutator;
parser::Walk(program, mutator);
return true;
}
using MaybeMsg = std::optional<parser::MessageFormattedText>;
static const llvm::StringSet<> warpFunctions_ = {"match_all_syncjj",
"match_all_syncjx", "match_all_syncjf", "match_all_syncjd",
"match_any_syncjj", "match_any_syncjx", "match_any_syncjf",
"match_any_syncjd"};
// These builtin procedures lower to inline pointer operations on either target.
static const llvm::StringSet<> inlinePointerFunctions_ = {"c_associated_c_ptr",
"c_associated_c_funptr", "__builtin_c_ptr_eq", "__builtin_c_ptr_ne",
"__builtin_c_devptr_eq", "__builtin_c_devptr_ne", "__builtin_c_funloc"};
enum class CallContext { Device, HostDevice, GuardedHostDevice };
// Match the BIND(C) intrinsic-module procedure intercepted by CUDA lowering,
// including calls through a USE rename. A user procedure named ON_DEVICE is
// an ordinary call and does not allow host-only or device-only callees.
static bool IsOnDevice(const evaluate::ProcedureDesignator &proc) {
const Symbol *sym{proc.GetSymbol()};
if (!sym) {
return false;
}
const Symbol &ultimate{sym->GetUltimate()};
const Symbol *module{ultimate.owner().GetSymbol()};
return ultimate.name() == "on_device" && IsBindCProcedure(ultimate) &&
module && module->attrs().test(Attr::INTRINSIC);
}
struct FindOnDevice : public evaluate::AnyTraverse<FindOnDevice, bool> {
using Base = evaluate::AnyTraverse<FindOnDevice, bool>;
FindOnDevice() : Base(*this) {}
using Base::operator();
bool operator()(const evaluate::ProcedureDesignator &proc) const {
return IsOnDevice(proc);
}
};
static bool ChecksOnDevice(
SemanticsContext &context, const parser::ScalarLogicalExpr &condition) {
const auto *expr{GetExpr(context, condition)};
return expr && FindOnDevice{}(*expr);
}
// Traverses an evaluate::Expr<> in search of unsupported operations
// on the device.
struct DeviceExprChecker
: public evaluate::AnyTraverse<DeviceExprChecker, MaybeMsg> {
using Result = MaybeMsg;
using Base = evaluate::AnyTraverse<DeviceExprChecker, Result>;
explicit DeviceExprChecker(SemanticsContext &c, CallContext callContext)
: Base(*this), context_{c}, callContext_{callContext} {}
using Base::operator();
Result operator()(const evaluate::ProcedureRef &x) const {
if (auto msg{CheckCallee(x.proc())}) {
return msg;
}
// A procedure passed as an argument is not called. Calls evaluated in
// argument expressions still need checking.
return (*this)(x.arguments());
}
private:
Result CheckCallee(const evaluate::ProcedureDesignator &x) const {
if (IsOnDevice(x)) {
return {};
}
if (const Symbol * sym{x.GetInterfaceSymbol()}) {
const Symbol &ultimate{sym->GetUltimate()};
const auto *subp{ultimate.detailsIf<semantics::SubprogramDetails>()};
if (subp && subp->moduleInterface()) {
subp = subp->moduleInterface()
->GetUltimate()
.detailsIf<semantics::SubprogramDetails>();
}
if (subp) {
if (const auto &stmtFunction{subp->stmtFunction()};
stmtFunction && IsCUDADeviceContext(&ultimate.owner())) {
return (*this)(*stmtFunction);
}
if (auto attrs{subp->cudaSubprogramAttrs()}) {
if (*attrs == common::CUDASubprogramAttrs::HostDevice ||
*attrs == common::CUDASubprogramAttrs::Device) {
if (warpFunctions_.contains(sym->name().ToString()) &&
!context_.languageFeatures().IsEnabled(
Fortran::common::LanguageFeature::CudaWarpMatchFunction)) {
return parser::MessageFormattedText(
"warp match function disabled"_err_en_US);
}
if (*attrs == common::CUDASubprogramAttrs::Device &&
callContext_ == CallContext::HostDevice) {
return CallError(x, true);
}
return {};
}
if (*attrs == common::CUDASubprogramAttrs::Global) {
return parser::MessageFormattedText(
"not yet implemented: CUDA dynamic parallelism"_err_en_US);
}
}
if (!subp->openACCRoutineInfos().empty()) {
return {};
}
}
const Scope &scope{ultimate.owner()};
const Symbol *mod{scope.IsModule() ? scope.symbol() : nullptr};
// Allow ieee_arithmetic module functions to be called on the device.
// TODO: Check for unsupported ieee_arithmetic on the device.
if (mod && mod->name() == "ieee_arithmetic") {
return {};
}
if (mod && mod->attrs().test(Attr::INTRINSIC) &&
mod->name() == "__fortran_builtins" &&
inlinePointerFunctions_.contains(ultimate.name().ToString())) {
return {};
}
} else if (x.GetSpecificIntrinsic()) {
// TODO(CUDA): Check for unsupported intrinsics here
return {};
}
if (callContext_ == CallContext::GuardedHostDevice) {
return {};
}
return CallError(x, false);
}
SemanticsContext &context_;
CallContext callContext_;
private:
static Result CallError(
const evaluate::ProcedureDesignator &proc, bool inHostCode) {
if (auto generic{proc.genericName()}) {
const Symbol *specific{proc.GetInterfaceSymbol()};
const std::string specificName{specific
? specific->GetUltimate().name().ToString()
: proc.GetName()};
if (*generic != specificName) {
return parser::MessageFormattedText(inHostCode
? "'%s' (specific procedure '%s') may not be called in host code"_err_en_US
: "'%s' (specific procedure '%s') may not be called in device code"_err_en_US,
*generic, specificName);
}
}
return parser::MessageFormattedText(inHostCode
? "'%s' may not be called in host code"_err_en_US
: "'%s' may not be called in device code"_err_en_US,
proc.GetName());
}
};
static bool IsHostArray(const Symbol &symbol) {
const Symbol &resolved{GetAssociationRoot(symbol)};
if (const auto *details{
resolved.detailsIf<semantics::ObjectEntityDetails>()}) {
if (details->cudaDataAttr() &&
(*details->cudaDataAttr() == common::CUDADataAttr::Device ||
*details->cudaDataAttr() == common::CUDADataAttr::Constant ||
*details->cudaDataAttr() == common::CUDADataAttr::Managed ||
*details->cudaDataAttr() == common::CUDADataAttr::Shared ||
*details->cudaDataAttr() == common::CUDADataAttr::Unified ||
*details->cudaDataAttr() == common::CUDADataAttr::UseDevice)) {
return false;
}
}
return true;
}
struct FindHostArray
: public evaluate::AnyTraverse<FindHostArray, const Symbol *> {
using Result = const Symbol *;
using Base = evaluate::AnyTraverse<FindHostArray, Result>;
FindHostArray() : Base(*this) {}
using Base::operator();
Result operator()(const evaluate::Component &x) const {
const Symbol &symbol{x.GetLastSymbol().GetUltimate()};
const Symbol &baseSymbol{GetAssociationRoot(x.base().GetFirstSymbol())};
if (symbol.IsFuncResult() || baseSymbol.IsFuncResult()) {
return nullptr;
}
if (!IsHostArray(symbol)) {
return nullptr;
}
if (IsDummy(baseSymbol) && IsCUDADeviceContext(&baseSymbol.owner())) {
return nullptr;
}
if (IsAllocatableOrPointer(symbol)) {
if (Result hostArray{(*this)(symbol)}) {
return hostArray;
}
} else if (const auto *details{symbol.GetUltimate()
.detailsIf<semantics::ObjectEntityDetails>()}) {
if (details->IsArray()) {
if (!IsHostArray(baseSymbol)) {
return nullptr;
}
return &symbol;
}
}
return (*this)(x.base());
}
Result operator()(const Symbol &symbol) const {
if (symbol.IsFuncResult()) {
return nullptr;
}
if (const auto *details{
symbol.GetUltimate().detailsIf<semantics::ObjectEntityDetails>()}) {
if (details->IsArray() &&
!symbol.attrs().test(Fortran::semantics::Attr::PARAMETER) &&
(!details->cudaDataAttr() ||
(details->cudaDataAttr() &&
*details->cudaDataAttr() != common::CUDADataAttr::Device &&
*details->cudaDataAttr() != common::CUDADataAttr::Constant &&
*details->cudaDataAttr() != common::CUDADataAttr::Managed &&
*details->cudaDataAttr() != common::CUDADataAttr::Shared &&
*details->cudaDataAttr() != common::CUDADataAttr::Unified &&
*details->cudaDataAttr() !=
common::CUDADataAttr::UseDevice))) {
return &symbol;
}
}
return nullptr;
}
};
// Intrinsics that only use the address or the descriptor of their arguments.
static const llvm::StringSet<> hostAddressIntrinsics_ = {
"__builtin_c_devloc", "__builtin_c_loc", "c_sizeof", "loc", "sizeof"};
// Inquiry intrinsics whose arguments are all inquired objects. Other inquiry
// intrinsics only inquire about their first argument and read the values of
// the others (DIM=, KIND=).
static const llvm::StringSet<> allArgsInquiryIntrinsics_ = {
"associated", "extends_type_of", "same_type_as"};
// Collects, in order of appearance, the device data whose value host code
// reads when it evaluates an expression. Device data that is only designated
// (actual argument to a procedure, argument of an inquiry intrinsic) is not
// read; the subscripts of its designator still are. A component with a CUDA
// data attribute is collected instead of its base.
struct CollectDeviceDataReadOnHost
: public evaluate::Traverse<CollectDeviceDataReadOnHost, SymbolVector> {
using Result = SymbolVector;
using Base = evaluate::Traverse<CollectDeviceDataReadOnHost, Result>;
explicit CollectDeviceDataReadOnHost(
SemanticsContext &c, bool onlyDesignated = false)
: Base(*this), context_{c}, onlyDesignated_{onlyDesignated} {}
using Base::operator();
static Result Default() { return {}; }
static Result Combine(Result &&x, Result &&y) {
x.insert(x.end(), y.begin(), y.end());
return std::move(x);
}
Result operator()(const Symbol &symbol) const {
if (onlyDesignated_ ||
!evaluate::IsCUDADeviceOnlySymbol(GetAssociationRoot(symbol))) {
return {};
}
return {symbol};
}
Result operator()(const evaluate::Component &x) const {
const Symbol &component{x.GetLastSymbol()};
if (evaluate::HasCUDADataAttr(component)) {
// The attribute of the component hides the one of the base.
return Combine((*this)(component),
CollectDeviceDataReadOnHost{context_, /*onlyDesignated=*/true}(
x.base()));
}
return (*this)(x.base());
}
Result operator()(const evaluate::ArrayRef &x) const {
return Combine((*this)(x.base()),
CollectDeviceDataReadOnHost{context_}(x.subscript()));
}
Result operator()(const evaluate::Substring &x) const {
CollectDeviceDataReadOnHost readCollector{context_};
return Combine((*this)(x.parent()),
Combine(readCollector(x.lower()), readCollector(x.upper())));
}
Result operator()(const evaluate::DescriptorInquiry &x) const {
// Accessing descriptor metadata is allowed, but selecting the descriptor
// may require reading device data in subscripts.
return CollectDeviceDataReadOnHost{context_, /*onlyDesignated=*/true}(
x.base());
}
Result operator()(const evaluate::TypeParamInquiry &) const { return {}; }
Result operator()(const evaluate::ProcedureRef &x) const {
bool onlyFirstArgDesignated{false};
if (const auto *intrinsic{x.proc().GetSpecificIntrinsic()}) {
if (context_.intrinsics().GetIntrinsicClass(intrinsic->name) !=
evaluate::IntrinsicClass::inquiryFunction &&
!hostAddressIntrinsics_.contains(intrinsic->name)) {
return (*this)(x.arguments());
}
onlyFirstArgDesignated =
!allArgsInquiryIntrinsics_.contains(intrinsic->name);
}
Result result;
for (std::size_t j{0}; j < x.arguments().size(); ++j) {
const auto &arg{x.arguments()[j]};
if (const auto *expr{arg ? arg->UnwrapExpr() : nullptr}) {
bool designated{
evaluate::IsVariable(*expr) && (j == 0 || !onlyFirstArgDesignated)};
result = Combine(std::move(result),
CollectDeviceDataReadOnHost{
context_, onlyDesignated_ || designated}(*expr));
}
}
return result;
}
SemanticsContext &context_;
bool onlyDesignated_{false};
};
// A scalar variable other than a component, an allocatable or a pointer.
static bool IsPlainScalar(const Symbol &symbol) {
const Symbol &ultimate{symbol.GetUltimate()};
return ultimate.has<ObjectEntityDetails>() && ultimate.Rank() == 0 &&
!ultimate.owner().IsDerivedType() && !IsAllocatableOrPointer(ultimate);
}
template <typename A>
static void CheckUnwrappedExpr(SemanticsContext &context, SourceName at,
const A &x, CallContext callContext) {
if (const auto *expr{parser::Unwrap<parser::Expr>(x)}) {
if (auto msg{DeviceExprChecker{context, callContext}(expr->typedExpr)}) {
context.Say(at, std::move(*msg));
}
}
}
template <bool CUF_KERNEL> struct ActionStmtChecker {
template <typename A>
static MaybeMsg WhyNotOk(
SemanticsContext &context, const A &x, CallContext callContext) {
if constexpr (ConstraintTrait<A>) {
return WhyNotOk(context, x.thing, callContext);
} else if constexpr (WrapperTrait<A>) {
return WhyNotOk(context, x.v, callContext);
} else if constexpr (UnionTrait<A>) {
return WhyNotOk(context, x.u, callContext);
} else if constexpr (TupleTrait<A>) {
return WhyNotOk(context, x.t, callContext);
} else {
return parser::MessageFormattedText{
"Statement may not appear in device code"_err_en_US};
}
}
template <typename A>
static MaybeMsg WhyNotOk(SemanticsContext &context,
const common::Indirection<A> &x, CallContext callContext) {
return WhyNotOk(context, x.value(), callContext);
}
template <typename... As>
static MaybeMsg WhyNotOk(SemanticsContext &context,
const std::variant<As...> &x, CallContext callContext) {
return common::visit(
[&context, callContext](
const auto &x) { return WhyNotOk(context, x, callContext); },
x);
}
template <std::size_t J = 0, typename... As>
static MaybeMsg WhyNotOk(SemanticsContext &context,
const std::tuple<As...> &x, CallContext callContext) {
if constexpr (J == sizeof...(As)) {
return {};
} else if (auto msg{WhyNotOk(context, std::get<J>(x), callContext)}) {
return msg;
} else {
return WhyNotOk<(J + 1)>(context, x, callContext);
}
}
template <typename A>
static MaybeMsg WhyNotOk(SemanticsContext &context, const std::list<A> &x,
CallContext callContext) {
for (const auto &y : x) {
if (MaybeMsg result{WhyNotOk(context, y, callContext)}) {
return result;
}
}
return {};
}
template <typename A>
static MaybeMsg WhyNotOk(SemanticsContext &context, const std::optional<A> &x,
CallContext callContext) {
if (x) {
return WhyNotOk(context, *x, callContext);
} else {
return {};
}
}
template <typename A>
static MaybeMsg WhyNotOk(SemanticsContext &context,
const parser::UnlabeledStatement<A> &x, CallContext callContext) {
return WhyNotOk(context, x.statement, callContext);
}
template <typename A>
static MaybeMsg WhyNotOk(SemanticsContext &context,
const parser::Statement<A> &x, CallContext callContext) {
return WhyNotOk(context, x.statement, callContext);
}
static MaybeMsg WhyNotOk(SemanticsContext &context,
const parser::AssignmentStmt &x, CallContext callContext) {
return DeviceExprChecker{context, callContext}(x.typedAssignment);
}
static MaybeMsg WhyNotOk(SemanticsContext &context, const parser::CallStmt &x,
CallContext callContext) {
return DeviceExprChecker{context, callContext}(x.typedCall);
}
static MaybeMsg WhyNotOk(SemanticsContext &context,
const parser::ContinueStmt &, CallContext callContext) {
return {};
}
static MaybeMsg WhyNotOk(
SemanticsContext &, const parser::PauseStmt &, CallContext callContext) {
return parser::MessageFormattedText{
"device subprograms may not contain PAUSE statements"_err_en_US};
}
static MaybeMsg WhyNotOk(SemanticsContext &context,
const parser::NullifyStmt &x, CallContext callContext) {
for (const auto &y : x.v) {
if (MaybeMsg result{
DeviceExprChecker{context, callContext}(y.typedExpr)}) {
return result;
}
}
return {};
}
static MaybeMsg WhyNotOk(SemanticsContext &context,
const parser::PointerAssignmentStmt &x, CallContext callContext) {
return DeviceExprChecker{context, callContext}(x.typedAssignment);
}
};
// Check analyzed expressions in statements and specification parts that do
// not have a single typed expression for the entire construct. Do not descend
// into an expression already checked, or into declarations of other entities
// whose expressions are not evaluated on entry to this scope.
struct DeviceExprVisitor {
SemanticsContext &context;
CallContext callContext;
template <typename A> bool Pre(const A &) { return true; }
template <typename A> void Post(const A &) {}
bool Pre(const parser::Expr &x) { return Check(x.typedExpr, x.source); }
bool Pre(const parser::Variable &x) {
return Check(x.typedExpr, x.GetSource());
}
bool Pre(const parser::InterfaceBlock &) { return false; }
bool Pre(const parser::DerivedTypeDef &) { return false; }
bool Pre(const parser::StmtFunctionStmt &) { return false; }
private:
bool Check(const parser::TypedExpr &expr, parser::CharBlock source) {
if (expr) {
if (auto msg{DeviceExprChecker{context, callContext}(expr)}) {
context.Say(source, std::move(*msg));
}
return false;
}
return true;
}
};
template <bool IsCUFKernelDo> class DeviceContextChecker {
public:
explicit DeviceContextChecker(SemanticsContext &c) : context_{c} {}
template <typename A>
void CheckSubprogram(const parser::Name &name, const A &header,
const parser::SpecificationPart &spec, const parser::Block &body) {
if (name.symbol) {
const auto *subp{
name.symbol->GetUltimate().detailsIf<SubprogramDetails>()};
if (subp && subp->moduleInterface()) {
subp = subp->moduleInterface()
->GetUltimate()
.detailsIf<SubprogramDetails>();
}
if (subp &&
subp->cudaSubprogramAttrs().value_or(
common::CUDASubprogramAttrs::Host) !=
common::CUDASubprogramAttrs::Host) {
isHostDevice = subp->cudaSubprogramAttrs() &&
subp->cudaSubprogramAttrs() ==
common::CUDASubprogramAttrs::HostDevice;
callContext_ =
isHostDevice ? CallContext::HostDevice : CallContext::Device;
CheckExpressions(header);
CheckExpressions(spec);
Check(body);
}
}
}
void Check(const parser::Block &block) {
// A guard established by a return affects only this block's continuation.
auto restore{common::Restorer{callContext_, callContext_}};
CheckStatements(block);
}
private:
void CheckStatements(const parser::Block &block) {
for (const auto &epc : block) {
Check(epc);
}
}
template <typename A> void CheckExpressions(const A &x) {
DeviceExprVisitor visitor{context_, callContext_};
parser::Walk(x, visitor);
}
void Check(const parser::ExecutionPartConstruct &epc) {
common::visit(
common::visitors{
[&](const parser::ExecutableConstruct &x) { Check(x); },
[&](const parser::Statement<common::Indirection<parser::EntryStmt>>
&x) {
context_.Say(x.source,
"Device code may not contain an ENTRY statement"_err_en_US);
},
[](const parser::Statement<common::Indirection<parser::FormatStmt>>
&) {},
[](const parser::Statement<common::Indirection<parser::DataStmt>>
&) {},
[](const parser::Statement<
common::Indirection<parser::NamelistStmt>> &) {},
[](const parser::ErrorRecovery &) {},
},
epc.u);
}
void Check(const parser::ExecutableConstruct &ec) {
common::visit(
common::visitors{
[&](const parser::Statement<parser::ActionStmt> &stmt) {
Check(stmt.statement, stmt.source);
},
[&](const common::Indirection<parser::DoConstruct> &x) {
if (const std::optional<parser::LoopControl> &control{
x.value().GetLoopControl()}) {
common::visit([&](const auto &y) { Check(y); }, control->u);
}
Check(std::get<parser::Block>(x.value().t));
},
[&](const common::Indirection<parser::BlockConstruct> &x) {
CheckExpressions(
std::get<parser::BlockSpecificationPart>(x.value().t));
// BLOCK executes unconditionally, so its guard also applies to
// the enclosing continuation.
CheckStatements(std::get<parser::Block>(x.value().t));
},
[&](const common::Indirection<parser::IfConstruct> &x) {
Check(x.value());
},
[&](const common::Indirection<parser::CaseConstruct> &x) {
CheckExpressions(
std::get<parser::Statement<parser::SelectCaseStmt>>(
x.value().t));
const auto &caseList{
std::get<std::list<parser::CaseConstruct::Case>>(
x.value().t)};
for (const parser::CaseConstruct::Case &c : caseList) {
Check(std::get<parser::Block>(c.t));
}
},
[&](const common::Indirection<parser::CompilerDirective> &x) {
// TODO(CUDA): Check for unsupported compiler directive here.
},
[&](const auto &x) {
if (auto source{parser::GetSource(x)}) {
context_.Say(*source,
"Statement may not appear in device code"_err_en_US);
}
},
},
ec.u);
}
template <typename SEEK, typename A>
static const SEEK *GetIOControl(const A &stmt) {
for (const auto &spec : stmt.controls) {
if (const auto *result{std::get_if<SEEK>(&spec.u)}) {
return result;
}
}
return nullptr;
}
template <typename A> static bool IsInternalIO(const A &stmt) {
if (stmt.iounit.has_value()) {
return std::holds_alternative<Fortran::parser::Variable>(stmt.iounit->u);
}
if (auto *unit{GetIOControl<Fortran::parser::IoUnit>(stmt)}) {
return std::holds_alternative<Fortran::parser::Variable>(unit->u);
}
return false;
}
void WarnOnIoStmt(const parser::CharBlock &source) {
context_.Warn(common::UsageWarning::CUDAUsage, source,
"I/O statement might not be supported on device"_warn_en_US);
}
template <typename A>
void WarnIfNotInternal(const A &stmt, const parser::CharBlock &source) {
if (!IsInternalIO(stmt)) {
WarnOnIoStmt(source);
}
}
template <typename A>
void ErrorIfHostSymbol(const A &expr, parser::CharBlock source) {
if (isHostDevice)
return;
if (context_.languageFeatures().IsEnabled(
common::LanguageFeature::CudaUnified))
return;
if (const Symbol * hostArray{FindHostArray{}(expr)}) {
context_.Say(source,
"Host array '%s' cannot be present in device context"_err_en_US,
hostArray->name());
}
}
void ErrorInCUFKernel(parser::CharBlock source) {
if (IsCUFKernelDo) {
context_.Say(
source, "Statement may not appear in cuf kernel code"_err_en_US);
}
}
void Check(const parser::ActionStmt &stmt, const parser::CharBlock &source) {
common::visit(
common::visitors{
[&](const common::Indirection<parser::CycleStmt> &) {
ErrorInCUFKernel(source);
},
[&](const common::Indirection<parser::ExitStmt> &) {
ErrorInCUFKernel(source);
},
[&](const common::Indirection<parser::GotoStmt> &) {
ErrorInCUFKernel(source);
},
[&](const common::Indirection<parser::StopStmt> &x) {
CheckExpressions(x.value());
},
[&](const common::Indirection<parser::PrintStmt> &x) {
CheckExpressions(x.value());
},
[&](const common::Indirection<parser::WriteStmt> &x) {
CheckExpressions(x.value());
if (x.value().format) { // Formatted write to '*' or '6'
if (std::holds_alternative<Fortran::parser::Star>(
x.value().format->u)) {
if (x.value().iounit) {
if (std::holds_alternative<Fortran::parser::Star>(
x.value().iounit->u)) {
return;
}
}
}
}
WarnIfNotInternal(x.value(), source);
},
[&](const common::Indirection<parser::CloseStmt> &x) {
CheckExpressions(x.value());
WarnOnIoStmt(source);
},
[&](const common::Indirection<parser::EndfileStmt> &x) {
CheckExpressions(x.value());
WarnOnIoStmt(source);
},
[&](const common::Indirection<parser::OpenStmt> &x) {
CheckExpressions(x.value());
WarnOnIoStmt(source);
},
[&](const common::Indirection<parser::ReadStmt> &x) {
CheckExpressions(x.value());
WarnIfNotInternal(x.value(), source);
},
[&](const common::Indirection<parser::InquireStmt> &x) {
CheckExpressions(x.value());
WarnOnIoStmt(source);
},
[&](const common::Indirection<parser::RewindStmt> &x) {
CheckExpressions(x.value());
WarnOnIoStmt(source);
},
[&](const common::Indirection<parser::BackspaceStmt> &x) {
CheckExpressions(x.value());
WarnOnIoStmt(source);
},
[&](const common::Indirection<parser::AllocateStmt> &x) {
CheckExpressions(x.value());
},
[&](const common::Indirection<parser::DeallocateStmt> &x) {
CheckExpressions(x.value());
},
[&](const common::Indirection<parser::IfStmt> &x) {
Check(x.value());
},
[&](const common::Indirection<parser::AssignmentStmt> &x) {
if (const evaluate::Assignment *
assign{semantics::GetAssignment(x.value())}) {
ErrorIfHostSymbol(assign->lhs, source);
ErrorIfHostSymbol(assign->rhs, source);
}
if (auto msg{ActionStmtChecker<IsCUFKernelDo>::WhyNotOk(
context_, x, callContext_)}) {
context_.Say(source, std::move(*msg));
}
},
[&](const auto &x) {
if (auto msg{ActionStmtChecker<IsCUFKernelDo>::WhyNotOk(
context_, x, callContext_)}) {
context_.Say(source, std::move(*msg));
}
},
},
stmt.u);
}
void Check(const parser::IfConstruct &ic) {
const CallContext incoming{callContext_};
const auto &ifS{std::get<parser::Statement<parser::IfThenStmt>>(ic.t)};
const auto &condition{std::get<parser::ScalarLogicalExpr>(ifS.statement.t)};
CheckUnwrappedExpr(context_, ifS.source, condition, callContext_);
AllowGuardedCalls(condition);
CallContext branchContext{callContext_};
Check(std::get<parser::Block>(ic.t));
const auto &elseIfBlocks{
std::get<std::list<parser::IfConstruct::ElseIfBlock>>(ic.t)};
for (const auto &eib : elseIfBlocks) {
// Later arms remain under the preceding conditions' guards. Reset any
// context changes caused by statements inside the preceding arm.
callContext_ = branchContext;
const auto &eIfS{std::get<parser::Statement<parser::ElseIfStmt>>(eib.t)};
const auto &elseIfCondition{
std::get<parser::ScalarLogicalExpr>(eIfS.statement.t)};
CheckUnwrappedExpr(context_, eIfS.source, elseIfCondition, callContext_);
AllowGuardedCalls(elseIfCondition);
branchContext = callContext_;
Check(std::get<parser::Block>(eib.t));
}
const auto &eb{
std::get<std::optional<parser::IfConstruct::ElseBlock>>(ic.t)};
if (eb) {
callContext_ = branchContext;
Check(std::get<parser::Block>(eb->t));
}
// A return only extends a guard when its arm is associated with ON_DEVICE.
if (!HasReturningOnDeviceArm(ic)) {
callContext_ = incoming;
}
}
void Check(const parser::IfStmt &is) {
const CallContext incoming{callContext_};
const auto &uS{
std::get<parser::UnlabeledStatement<parser::ActionStmt>>(is.t)};
const auto &condition{std::get<parser::ScalarLogicalExpr>(is.t)};
CheckUnwrappedExpr(context_, uS.source, condition, callContext_);
AllowGuardedCalls(condition);
Check(uS.statement, uS.source);
if (!IsReturn(uS.statement)) {
callContext_ = incoming;
}
}
static bool IsReturn(const parser::ActionStmt &stmt) {
const auto *ret{parser::Unwrap<parser::ReturnStmt>(stmt)};
return ret && !ret->v;
}
static bool HasUnconditionalReturn(const parser::Block &block) {
for (const auto &epc : block) {
if (const auto *stmt{parser::Unwrap<parser::ActionStmt>(epc)}) {
if (IsReturn(*stmt)) {
return true;
}
} else if (const auto *bc{parser::Unwrap<parser::BlockConstruct>(epc)}) {
if (HasUnconditionalReturn(std::get<parser::Block>(bc->t))) {
return true;
}
}
}
return false;
}
bool HasReturningOnDeviceArm(const parser::IfConstruct &ic) {
const auto &ifS{std::get<parser::Statement<parser::IfThenStmt>>(ic.t)};
bool checksOnDevice{ChecksOnDevice(
context_, std::get<parser::ScalarLogicalExpr>(ifS.statement.t))};
if (checksOnDevice &&
HasUnconditionalReturn(std::get<parser::Block>(ic.t))) {
return true;
}
for (const auto &eib :
std::get<std::list<parser::IfConstruct::ElseIfBlock>>(ic.t)) {
const auto &elseIfS{
std::get<parser::Statement<parser::ElseIfStmt>>(eib.t)};
checksOnDevice = ChecksOnDevice(
context_, std::get<parser::ScalarLogicalExpr>(elseIfS.statement.t));
if (checksOnDevice &&
HasUnconditionalReturn(std::get<parser::Block>(eib.t))) {
return true;
}
}
const auto &eb{
std::get<std::optional<parser::IfConstruct::ElseBlock>>(ic.t)};
// A plain ELSE is associated with the preceding IF or ELSEIF condition.
return checksOnDevice && eb &&
HasUnconditionalReturn(std::get<parser::Block>(eb->t));
}
void AllowGuardedCalls(const parser::ScalarLogicalExpr &condition) {
// Accept either kind of callee in either arm of an ON_DEVICE guard.
// Determining whether a call is on the appropriate side is deferred.
if (isHostDevice && ChecksOnDevice(context_, condition)) {
callContext_ = CallContext::GuardedHostDevice;
}
}
void Check(const parser::LoopControl::Bounds &bounds) {
Check(bounds.Lower());
Check(bounds.Upper());
if (auto &step{bounds.Step()}) {
Check(*step);
}
}
void Check(const parser::LoopControl::Concurrent &x) {
const auto &header{std::get<parser::ConcurrentHeader>(x.t)};
for (const auto &cc :
std::get<std::list<parser::ConcurrentControl>>(header.t)) {
Check(std::get<1>(cc.t));
Check(std::get<2>(cc.t));
if (const auto &step{
std::get<std::optional<parser::ScalarIntExpr>>(cc.t)}) {
Check(*step);
}
}
if (const auto &mask{
std::get<std::optional<parser::ScalarLogicalExpr>>(header.t)}) {
Check(*mask);
}
}
void Check(const parser::ScalarLogicalExpr &x) {
Check(DEREF(parser::Unwrap<parser::Expr>(x)));
}
void Check(const parser::ScalarIntExpr &x) {
Check(DEREF(parser::Unwrap<parser::Expr>(x)));
}
void Check(const parser::ScalarExpr &x) {
Check(DEREF(parser::Unwrap<parser::Expr>(x)));
}
void Check(const parser::Expr &expr) {
if (MaybeMsg msg{
DeviceExprChecker{context_, callContext_}(expr.typedExpr)}) {
context_.Say(expr.source, std::move(*msg));
}
}
SemanticsContext &context_;
bool isHostDevice{false};
CallContext callContext_{CallContext::Device};
};
void CUDAChecker::Enter(const parser::SubroutineSubprogram &x) {
DeviceContextChecker<false>{context_}.CheckSubprogram(
std::get<parser::Name>(
std::get<parser::Statement<parser::SubroutineStmt>>(x.t).statement.t),
std::get<parser::Statement<parser::SubroutineStmt>>(x.t).statement,
std::get<parser::SpecificationPart>(x.t),
std::get<parser::ExecutionPart>(x.t).v);
}
void CUDAChecker::Enter(const parser::FunctionSubprogram &x) {
DeviceContextChecker<false>{context_}.CheckSubprogram(
std::get<parser::Name>(
std::get<parser::Statement<parser::FunctionStmt>>(x.t).statement.t),
std::get<parser::Statement<parser::FunctionStmt>>(x.t).statement,
std::get<parser::SpecificationPart>(x.t),
std::get<parser::ExecutionPart>(x.t).v);
}
void CUDAChecker::Enter(const parser::SeparateModuleSubprogram &x) {
DeviceContextChecker<false>{context_}.CheckSubprogram(
std::get<parser::Statement<parser::MpSubprogramStmt>>(x.t).statement.v,
std::get<parser::Statement<parser::MpSubprogramStmt>>(x.t).statement,
std::get<parser::SpecificationPart>(x.t),
std::get<parser::ExecutionPart>(x.t).v);
}
// !$CUF KERNEL DO semantic checks
static int DoConstructTightNesting(
const parser::DoConstruct *doConstruct, const parser::Block *&innerBlock) {
if (!doConstruct ||
(!doConstruct->IsDoNormal() && !doConstruct->IsDoConcurrent())) {
return 0;
}
innerBlock = &std::get<parser::Block>(doConstruct->t);
if (doConstruct->IsDoConcurrent()) {
const auto &loopControl = doConstruct->GetLoopControl();
if (loopControl) {
if (const auto *concurrentControl{
std::get_if<parser::LoopControl::Concurrent>(&loopControl->u)}) {
const auto &concurrentHeader =
std::get<Fortran::parser::ConcurrentHeader>(concurrentControl->t);
const auto &controls =
std::get<std::list<Fortran::parser::ConcurrentControl>>(
concurrentHeader.t);
return controls.size();
}
}
return 0;
}
if (innerBlock->size() == 1) {
if (const auto *execConstruct{
std::get_if<parser::ExecutableConstruct>(&innerBlock->front().u)}) {
if (const auto *next{
std::get_if<common::Indirection<parser::DoConstruct>>(
&execConstruct->u)}) {
return 1 + DoConstructTightNesting(&next->value(), innerBlock);
}
}
}
return 1;
}
static void CheckReduce(
SemanticsContext &context, const parser::CUFReduction &reduce) {
auto op{std::get<parser::CUFReduction::Operator>(reduce.t).v};
for (const auto &var :
std::get<std::list<parser::Scalar<parser::Variable>>>(reduce.t)) {
if (const auto &typedExprPtr{var.thing.typedExpr};
typedExprPtr && typedExprPtr->v) {
const auto &expr{*typedExprPtr->v};
if (auto type{expr.GetType()}) {
auto cat{type->category()};
bool isOk{false};
switch (op) {
case parser::ReductionOperator::Operator::Minus:
context.Say(var.thing.GetSource(),
"'-' is not a supported !$CUF KERNEL DO REDUCE operator"_err_en_US);
continue;
case parser::ReductionOperator::Operator::Plus:
case parser::ReductionOperator::Operator::Multiply:
case parser::ReductionOperator::Operator::Max:
case parser::ReductionOperator::Operator::Min:
isOk = cat == TypeCategory::Integer || cat == TypeCategory::Real ||
cat == TypeCategory::Complex;
break;
case parser::ReductionOperator::Operator::Iand:
case parser::ReductionOperator::Operator::Ior:
case parser::ReductionOperator::Operator::Ieor:
isOk = cat == TypeCategory::Integer;
break;
case parser::ReductionOperator::Operator::And:
case parser::ReductionOperator::Operator::Or:
case parser::ReductionOperator::Operator::Eqv:
case parser::ReductionOperator::Operator::Neqv:
isOk = cat == TypeCategory::Logical;
break;
}
if (!isOk) {
context.Say(var.thing.GetSource(),
"!$CUF KERNEL DO REDUCE operation is not acceptable for a variable with type %s"_err_en_US,
type->AsFortran());
}
}
}
}
}
void CUDAChecker::Enter(const parser::CUFKernelDoConstruct &x) {
auto source{std::get<parser::CUFKernelDoConstruct::Directive>(x.t).source};
const auto &directive{std::get<parser::CUFKernelDoConstruct::Directive>(x.t)};
std::int64_t depth{1};
if (auto expr{AnalyzeExpr(context_,
std::get<std::optional<parser::ScalarIntConstantExpr>>(
directive.t))}) {
depth = evaluate::ToInt64(expr).value_or(0);
if (depth <= 0) {
context_.Say(source,
"!$CUF KERNEL DO (%jd): loop nesting depth must be positive"_err_en_US,
std::intmax_t{depth});
depth = 1;
}
}
const parser::DoConstruct *doConstruct{common::GetPtrFromOptional(
std::get<std::optional<parser::DoConstruct>>(x.t))};
const parser::Block *innerBlock{nullptr};
if (DoConstructTightNesting(doConstruct, innerBlock) < depth) {
if (doConstruct && doConstruct->IsDoConcurrent())
context_.Say(source,
"!$CUF KERNEL DO (%jd) must be followed by a DO CONCURRENT construct with at least %jd indices"_err_en_US,
std::intmax_t{depth}, std::intmax_t{depth});
else
context_.Say(source,
"!$CUF KERNEL DO (%jd) must be followed by a DO construct with tightly nested outer levels of counted DO loops"_err_en_US,
std::intmax_t{depth});
}
if (innerBlock) {
DeviceContextChecker<true>{context_}.Check(*innerBlock);
}
for (const auto &reduce :
std::get<std::list<parser::CUFReduction>>(directive.t)) {
CheckReduce(context_, reduce);
}
++deviceConstructDepth_;
}
static bool IsOpenACCComputeConstruct(const parser::OpenACCBlockConstruct &x) {
const auto &beginBlockDirective =
std::get<Fortran::parser::AccBeginBlockDirective>(x.t);
const auto &blockDirective =
std::get<Fortran::parser::AccBlockDirective>(beginBlockDirective.t);
if (blockDirective.v == llvm::acc::ACCD_parallel ||
blockDirective.v == llvm::acc::ACCD_serial ||
blockDirective.v == llvm::acc::ACCD_kernels) {
return true;
}
return false;
}
void CUDAChecker::Leave(const parser::CUFKernelDoConstruct &) {
--deviceConstructDepth_;
}
void CUDAChecker::Enter(const parser::OpenACCBlockConstruct &x) {
if (IsOpenACCComputeConstruct(x)) {
++deviceConstructDepth_;
}
}
void CUDAChecker::Leave(const parser::OpenACCBlockConstruct &x) {
if (IsOpenACCComputeConstruct(x)) {
--deviceConstructDepth_;
}
}
void CUDAChecker::Enter(const parser::OpenACCCombinedConstruct &) {
++deviceConstructDepth_;
}
void CUDAChecker::Leave(const parser::OpenACCCombinedConstruct &) {
--deviceConstructDepth_;
}
void CUDAChecker::Enter(const parser::OpenACCLoopConstruct &) {
++deviceConstructDepth_;
}
void CUDAChecker::Leave(const parser::OpenACCLoopConstruct &) {
--deviceConstructDepth_;
}
void CUDAChecker::Enter(const parser::DoConstruct &x) {
if (x.IsDoConcurrent() &&
context_.foldingContext().languageFeatures().IsEnabled(
common::LanguageFeature::DoConcurrentOffload)) {
++deviceConstructDepth_;
}
}
void CUDAChecker::Leave(const parser::DoConstruct &x) {
if (x.IsDoConcurrent() &&
context_.foldingContext().languageFeatures().IsEnabled(
common::LanguageFeature::DoConcurrentOffload)) {
--deviceConstructDepth_;
}
}
void CUDAChecker::Enter(const parser::AssignmentStmt &x) {
auto lhsLoc{std::get<parser::Variable>(x.t).GetSource()};
const auto &scope{context_.FindScope(lhsLoc)};
const Scope &progUnit{GetProgramUnitContaining(scope)};
if (IsCUDADeviceContext(&progUnit) || deviceConstructDepth_ > 0) {
return; // Data transfer with assignment is only perform on host.
}
const evaluate::Assignment *assign{semantics::GetAssignment(x)};
if (!assign) {
return;
}
int nbLhs{evaluate::GetNbOfCUDADeviceSymbols(assign->lhs)};
int nbRhs{evaluate::GetNbOfUniqueCUDADeviceSymbols(assign->rhs)};
int nbRhsManaged{
evaluate::GetNbOfUniqueCUDAManagedOrUnifiedSymbols(assign->rhs)};
// device to host transfer with more than one device object on the rhs is not
// legal. Managed and unified objects are accessible from the host and are not
// counted.
if (nbLhs == 0 && nbRhs - nbRhsManaged > 1) {
context_.Say(lhsLoc,
"More than one reference to a CUDA object on the right hand side of the assignment"_err_en_US);
}
// An implicit data transfer copies the whole device object to the host, and
// the size of an assumed-size array is unknown.
if (evaluate::IsCUDADataTransfer(assign->lhs, assign->rhs) &&
evaluate::HasCUDAImplicitTransfer(assign->rhs)) {
for (const Symbol &sym : evaluate::CollectCudaSymbols(assign->rhs)) {
if (evaluate::IsCUDADeviceSymbol(sym) &&
IsAssumedSizeArray(sym.GetUltimate())) {
context_.Say(lhsLoc,
"Implicit data transfer of assumed-size device array '%s' is not supported"_err_en_US,
sym.name());
break;
}
}
}
if (evaluate::HasCUDADeviceAttrs(assign->lhs) &&
(evaluate::HasCUDAImplicitTransfer(assign->rhs) &&
!evaluate::HasOnlyCUDAConstntImplicitTransfer(assign->rhs))) {
if (GetNbOfCUDAManagedOrUnifiedSymbols(assign->lhs) == 1 &&
GetNbOfCUDAManagedOrUnifiedSymbols(assign->rhs) == 1 && nbRhs == 1) {
return; // This is a special case handled on the host.
}
context_.Say(lhsLoc, "Unsupported CUDA data transfer"_err_en_US);
}
}
template <typename A> void CUDAChecker::EnterHostScalarExpr(const A &x) {
if (hostScalarExprDepth_++ > 0) {
return; // Checked with the enclosing expression.
}
const auto &expr{DEREF(parser::Unwrap<parser::Expr>(x))};
const Scope &progUnit{
GetProgramUnitContaining(context_.FindScope(expr.source))};
if (IsCUDADeviceContext(&progUnit) || deviceConstructDepth_ > 0) {
return;
}
if (const auto *typedExpr{GetExpr(context_, expr)}) {
const Symbol *deviceScalar{nullptr};
for (const Symbol &deviceData :
CollectDeviceDataReadOnHost{context_}(*typedExpr)) {
if (!IsPlainScalar(deviceData)) {
context_.Say(expr.source,
"Device data '%s' may not be referenced in host code outside of a data transfer or an actual argument"_err_en_US,
deviceData.name());
return;
}
// Host code reads a module scalar from its host copy.
if (!deviceScalar &&
deviceData.GetUltimate().owner().kind() != Scope::Kind::Module) {
deviceScalar = &deviceData;
}
}
if (deviceScalar) {
context_.Warn(common::UsageWarning::CUDAUsage, expr.source,
"Device data '%s' is read in host code outside of a data transfer or an actual argument"_warn_en_US,
deviceScalar->name());
}
}
}
void CUDAChecker::Enter(const parser::Scalar<parser::Expr> &x) {
EnterHostScalarExpr(x);
}
void CUDAChecker::Leave(const parser::Scalar<parser::Expr> &) {
--hostScalarExprDepth_;
}
void CUDAChecker::Enter(const parser::ScalarExpr &x) { EnterHostScalarExpr(x); }
void CUDAChecker::Leave(const parser::ScalarExpr &) { --hostScalarExprDepth_; }
void CUDAChecker::Enter(const parser::ScalarIntExpr &x) {
EnterHostScalarExpr(x);
}
void CUDAChecker::Leave(const parser::ScalarIntExpr &) {
--hostScalarExprDepth_;
}
void CUDAChecker::Enter(const parser::ScalarLogicalExpr &x) {
EnterHostScalarExpr(x);
}
void CUDAChecker::Leave(const parser::ScalarLogicalExpr &) {
--hostScalarExprDepth_;
}
void CUDAChecker::Enter(const parser::PrintStmt &x) {
CHECK(context_.location());
const Scope &scope{context_.FindScope(*context_.location())};
const Scope &progUnit{GetProgramUnitContaining(scope)};
if (IsCUDADeviceContext(&progUnit) || deviceConstructDepth_ > 0) {
return;
}
auto &outputItemList{std::get<std::list<Fortran::parser::OutputItem>>(x.t)};
for (const auto &item : outputItemList) {
if (const auto *x{std::get_if<parser::Expr>(&item.u)}) {
if (const auto *expr{GetExpr(context_, *x)}) {
for (const Symbol &sym : CollectCudaSymbols(*expr)) {
if (const auto *details = sym.GetUltimate()
.detailsIf<semantics::ObjectEntityDetails>()) {
if (details->cudaDataAttr() &&
(*details->cudaDataAttr() == common::CUDADataAttr::Device ||
*details->cudaDataAttr() ==
common::CUDADataAttr::Constant ||
*details->cudaDataAttr() ==
common::CUDADataAttr::UseDevice)) {
context_.Say(parser::FindSourceLocation(*x),
"device data not allowed in I/O statements"_err_en_US);
}
}
}
}
}
}
}
} // namespace Fortran::semantics