[flang] AMAX0, MIN1... rewrite to MAX/MIN: make result conversion explicit

Summary:
This patch changes speficic extremum functions rewrite to generic MIN/MAX.
It applies to AMAX0, AMIN0, AMAX1, AMIN1, MAX0, MIN0, MAX1, MIN1, DMAX1,
and DMIN1.

- Do not re-write specific extremums to MAX/MIN in intrinsic Probe and let
folding rewrite it and introduc the conversion on the MIN/MAX result.
- Also make operand promotion explicit in MIN/MAX folding.

For instance, after this patch:
AMAX0(int8, int4) is rewritten to REAL(MAX(int8, INT(int4, 8)))

All this care is to avoid rewritting it to MAX(REAL(int8), REAL(int4))
that may not always be numerically equivalent to the first rewrite.

Reviewers: klausler, schweitz, sscalpone, jdoerfert, DavidTruby

Reviewed By: klausler, schweitz

Subscribers: llvm-commits, flang-commits

Tags: #flang, #llvm

Differential Revision: https://reviews.llvm.org/D81940
This commit is contained in:
Jean Perier 2020-06-18 07:46:19 +02:00
parent f7455da263
commit f1fa3b7f6e
5 changed files with 97 additions and 7 deletions

View File

@ -656,13 +656,15 @@ template <typename T>
Expr<T> FoldMINorMAX(
FoldingContext &context, FunctionRef<T> &&funcRef, Ordering order) {
std::vector<Constant<T> *> constantArgs;
// Call Folding on all arguments, even if some are not constant,
// to make operand promotion explicit.
for (auto &arg : funcRef.arguments()) {
if (auto *cst{Folder<T>{context}.Folding(arg)}) {
constantArgs.push_back(cst);
} else {
return Expr<T>(std::move(funcRef));
}
}
if (constantArgs.size() != funcRef.arguments().size())
return Expr<T>(std::move(funcRef));
CHECK(constantArgs.size() > 0);
Expr<T> result{std::move(*constantArgs[0])};
for (std::size_t i{1}; i < constantArgs.size(); ++i) {
@ -672,6 +674,52 @@ Expr<T> FoldMINorMAX(
return result;
}
// For AMAX0, AMIN0, AMAX1, AMIN1, DMAX1, DMIN1, MAX0, MIN0, MAX1, and MIN1
// a special care has to be taken to insert the conversion on the result
// of the MIN/MAX. This is made slightly more complex by the extension
// supported by f18 that arguments may have different kinds. This implies
// that the created MIN/MAX result type cannot be deduced from the standard but
// has to be deduced from the arguments.
// e.g. AMAX0(int8, int4) is rewritten to REAL(MAX(int8, INT(int4, 8)))).
template <typename T>
Expr<T> RewriteSpecificMINorMAX(
FoldingContext &context, FunctionRef<T> &&funcRef) {
ActualArguments &args{funcRef.arguments()};
auto &intrinsic{DEREF(std::get_if<SpecificIntrinsic>(&funcRef.proc().u))};
// Rewrite MAX1(args) to INT(MAX(args)) and fold. Same logic for MIN1.
// Find result type for max/min based on the arguments.
DynamicType resultType{args[0].value().GetType().value()};
auto *resultTypeArg{&args[0]};
for (auto j{args.size() - 1}; j > 0; --j) {
DynamicType type{args[j].value().GetType().value()};
if (type.category() == resultType.category()) {
if (type.kind() > resultType.kind()) {
resultTypeArg = &args[j];
resultType = type;
}
} else if (resultType.category() == TypeCategory::Integer) {
// Handle mixed real/integer arguments: all the previous arguments were
// integers and this one is real. The type of the MAX/MIN result will
// be the one of the real argument.
resultTypeArg = &args[j];
resultType = type;
}
}
intrinsic.name =
intrinsic.name.find("max") != std::string::npos ? "max"s : "min"s;
intrinsic.characteristics.value().functionResult.value().SetType(resultType);
auto insertConversion{[&](const auto &x) -> Expr<T> {
using TR = ResultType<decltype(x)>;
FunctionRef<TR> maxRef{std::move(funcRef.proc()), std::move(args)};
return Fold(context, ConvertToType<T>(AsCategoryExpr(std::move(maxRef))));
}};
if (auto *sx{UnwrapExpr<Expr<SomeReal>>(*resultTypeArg)}) {
return std::visit(insertConversion, sx->u);
}
auto &sx{DEREF(UnwrapExpr<Expr<SomeInteger>>(*resultTypeArg))};
return std::visit(insertConversion, sx.u);
}
template <typename T>
Expr<T> FoldOperation(FoldingContext &context, FunctionRef<T> &&funcRef) {
ActualArguments &args{funcRef.arguments()};

View File

@ -423,6 +423,8 @@ Expr<Type<TypeCategory::Integer, KIND>> FoldIntrinsicFunction(
}));
} else if (name == "max") {
return FoldMINorMAX(context, std::move(funcRef), Ordering::Greater);
} else if (name == "max0" || name == "max1") {
return RewriteSpecificMINorMAX(context, std::move(funcRef));
} else if (name == "maxexponent") {
if (auto *sx{UnwrapExpr<Expr<SomeReal>>(args[0])}) {
return std::visit(
@ -448,6 +450,8 @@ Expr<Type<TypeCategory::Integer, KIND>> FoldIntrinsicFunction(
}
} else if (name == "min") {
return FoldMINorMAX(context, std::move(funcRef), Ordering::Less);
} else if (name == "min0" || name == "min1") {
return RewriteSpecificMINorMAX(context, std::move(funcRef));
} else if (name == "mod") {
return FoldElementalIntrinsic<T, T, T>(context, std::move(funcRef),
ScalarFuncWithContext<T, T, T>(

View File

@ -37,6 +37,9 @@ Expr<Type<TypeCategory::Real, KIND>> FoldIntrinsicFunction(
context.messages().Say(
"%s(real(kind=%d)) cannot be folded on host"_en_US, name, KIND);
}
} else if (name == "amax0" || name == "amin0" || name == "amin1" ||
name == "amax1" || name == "dmin1" || name == "dmax1") {
return RewriteSpecificMINorMAX(context, std::move(funcRef));
} else if (name == "atan" || name == "atan2" || name == "hypot" ||
name == "mod") {
std::string localName{name == "atan2" ? "atan" : name};

View File

@ -753,14 +753,24 @@ struct SpecificIntrinsicInterface : public IntrinsicInterface {
// Exact actual/dummy type matching is required by default for specific
// intrinsics. If useGenericAndForceResultType is set, then the probing will
// also attempt to use the related generic intrinsic and to convert the result
// to the specific intrinsic result type if needed.
// to the specific intrinsic result type if needed. This also prevents
// using the generic name so that folding can insert the conversion on the
// result and not the arguments.
//
// This is not enabled on all specific intrinsics because an alternative
// is to convert the actual arguments to the required dummy types and this is
// not numerically equivalent.
// e.g. IABS(INT(i), INT(j)) not equiv to INT(ABS(i, j)).
// e.g. IABS(INT(i, 4)) not equiv to INT(ABS(i), 4).
// This is allowed for restricted min/max specific functions because
// the expected behavior is clear from their definitions. A warning is though
// always emitted because other compilers' behavior is not ubiquitous here.
// always emitted because other compilers' behavior is not ubiquitous here and
// the results in case of conversion overflow might not be equivalent.
// e.g for MIN0: INT(MIN(2147483647_8, 2*2147483647_8), 4) = 2147483647_4
// but: MIN(INT(2147483647_8, 4), INT(2*2147483647_8, 4)) = -2_4
// xlf and ifort return the first, and pgfortran the later. f18 will return
// the first because this matches more closely the MIN0 definition in
// Fortran 2018 table 16.3 (although it is still an extension to allow
// non default integer argument in MIN0).
bool useGenericAndForceResultType{false};
};
@ -1948,7 +1958,9 @@ std::optional<SpecificCall> IntrinsicProcTable::Implementation::Probe(
if (const char *genericName{specIter->second->generic}) {
if (auto specificCall{
matchOrBufferMessages(*specIter->second, specificBuffer)}) {
specificCall->specificIntrinsic.name = genericName;
if (!specIter->second->useGenericAndForceResultType) {
specificCall->specificIntrinsic.name = genericName;
}
specificCall->specificIntrinsic.isRestrictedSpecific =
specIter->second->isRestrictedSpecific;
// TODO test feature AdditionalIntrinsics, warn on nonstandard
@ -1973,10 +1985,11 @@ std::optional<SpecificCall> IntrinsicProcTable::Implementation::Probe(
// Force the call result type to the specific intrinsic result type
DynamicType newType{GetReturnType(*specIter->second, defaults_)};
context.messages().Say(
"Argument type does not match specific intrinsic '%s' "
"argument types do not match specific intrinsic '%s' "
"requirements; using '%s' generic instead and converting the "
"result to %s if needed"_en_US,
call.name, genericName, newType.AsFortran());
specificCall->specificIntrinsic.name = call.name;
specificCall->specificIntrinsic.characteristics.value()
.functionResult.value()
.SetType(newType);

View File

@ -44,3 +44,25 @@ module parentheses
real(4), parameter :: x_p = (x_nop)
logical, parameter :: test_parentheses1 = acos(x_p).EQ.acos(x_nop)
end module
module specific_extremums
! f18 accepts all type kinds for the arguments of specific extremum intrinsics
! instead of of only default kind (or double precision for DMAX1 and DMIN1).
! This extensions is implemented by using the related generic intrinsic and
! converting the result.
! The tests below are cases where an implementation that converts the arguments to the
! standard required types instead would give different results than the implementation
! specified for f18 (converting the result).
integer(8), parameter :: max_i32_8 = 2_8**31-1
integer, parameter :: expected_min0 = int(min(max_i32_8, 2_8*max_i32_8), 4)
!WARN: argument types do not match specific intrinsic 'min0' requirements; using 'min' generic instead and converting the result to INTEGER(4) if needed
integer, parameter :: result_min0 = min0(max_i32_8, 2_8*max_i32_8)
! result_min0 would be -2 if arguments were converted to default integer.
logical, parameter :: test_min0 = expected_min0 .EQ. result_min0
real, parameter :: expected_amax0 = real(max(max_i32_8, 2_8*max_i32_8), 4)
!WARN: argument types do not match specific intrinsic 'amax0' requirements; using 'max' generic instead and converting the result to REAL(4) if needed
real, parameter :: result_amax0 = amax0(max_i32_8, 2_8*max_i32_8)
! result_amax0 would be 2.1474836E+09 if arguments were converted to default integer first.
logical, parameter :: test_amax0 = expected_amax0 .EQ. result_amax0
end module