From e46cd9ee18d5d809123f4984fa23df9648f2fa8e Mon Sep 17 00:00:00 2001 From: peter klausler Date: Thu, 21 Nov 2019 13:31:52 -0800 Subject: [PATCH] [flang] Fold MERGE, use it in ISO_FORTRAN_ENV Reduce use of intrinsic type KIND codes in ISO_FORTRAN_ENV Use COUNT([x]) rather than MERGE(1,0,x) Fix problems in array constructor expression analysis Original-commit: flang-compiler/f18@79018f7126a55a4b8a907f121cc099d5f4f9666a Reviewed-on: https://github.com/flang-compiler/f18/pull/836 --- flang/lib/evaluate/fold.cc | 51 +++++-- flang/lib/semantics/expression.cc | 233 +++++++++++++++--------------- flang/module/iso_fortran_env.f90 | 116 +++++++++++++-- 3 files changed, 260 insertions(+), 140 deletions(-) diff --git a/flang/lib/evaluate/fold.cc b/flang/lib/evaluate/fold.cc index cf0f09776286..3e311227d32a 100644 --- a/flang/lib/evaluate/fold.cc +++ b/flang/lib/evaluate/fold.cc @@ -364,7 +364,6 @@ Expr Reshape(FoldingContext &context, FunctionRef &&funcRef) { std::optional> shape{ GetIntegerVector(args[1])}; std::optional> order{GetIntegerVector(args[3])}; - if (!source || !shape || (args[2] && !pad) || (args[3] && !order)) { return Expr{std::move(funcRef)}; // Non-constant arguments } else if (!IsValidShape(shape.value())) { @@ -565,6 +564,17 @@ Expr FoldOperation(FoldingContext &context, FunctionRef &&funcRef) { return Expr{std::move(funcRef)}; } +template +Expr FoldMerge(FoldingContext &context, FunctionRef &&funcRef) { + return FoldElementalIntrinsic(context, + std::move(funcRef), + ScalarFunc( + [](const Scalar &ifTrue, const Scalar &ifFalse, + const Scalar &predicate) -> Scalar { + return predicate.IsTrue() ? ifTrue : ifFalse; + })); +} + template Expr> FoldIntrinsicFunction( FoldingContext &context, @@ -587,6 +597,18 @@ Expr> FoldIntrinsicFunction( })); } else if (name == "bit_size") { return Expr{Scalar::bits}; + } else if (name == "count") { + if (!args[1]) { // TODO: COUNT(x,DIM=d) + if (const auto *constant{UnwrapConstantValue(args[0])}) { + std::int64_t result{0}; + for (const auto &element : constant->values()) { + if (element.IsTrue()) { + ++result; + } + } + return Expr{result}; + } + } } else if (name == "digits") { if (const auto *cx{UnwrapExpr>(args[0])}) { return Expr{std::visit( @@ -780,6 +802,8 @@ Expr> FoldIntrinsicFunction( }, sx->u); } + } else if (name == "merge") { + return FoldMerge(context, std::move(funcRef)); } else if (name == "merge_bits") { return FoldElementalIntrinsic( context, std::move(funcRef), &Scalar::MERGE_BITS); @@ -898,9 +922,9 @@ Expr> FoldIntrinsicFunction( return UBOUND(context, std::move(funcRef)); } // TODO: - // ceiling, count, cshift, dot_product, eoshift, + // ceiling, cshift, dot_product, eoshift, // findloc, floor, iall, iany, iparity, ibits, image_status, index, ishftc, - // len_trim, matmul, max, maxloc, maxval, merge, min, + // len_trim, matmul, maxloc, maxval, // minloc, minval, mod, modulo, nint, not, pack, product, reduce, // scan, sign, spread, sum, transfer, transpose, unpack, verify return Expr{std::move(funcRef)}; @@ -1037,6 +1061,8 @@ Expr> FoldIntrinsicFunction( return Expr{Scalar::HUGE()}; } else if (name == "max") { return FoldMINorMAX(context, std::move(funcRef), Ordering::Greater); + } else if (name == "merge") { + return FoldMerge(context, std::move(funcRef)); } else if (name == "min") { return FoldMINorMAX(context, std::move(funcRef), Ordering::Less); } else if (name == "real") { @@ -1050,7 +1076,7 @@ Expr> FoldIntrinsicFunction( return Expr{Scalar::TINY()}; } // TODO: anint, cshift, dim, dot_product, eoshift, fraction, matmul, - // max, maxval, merge, min, minval, modulo, nearest, norm2, pack, product, + // maxval, minval, modulo, nearest, norm2, pack, product, // reduce, rrspacing, scale, set_exponent, spacing, spread, // sum, transfer, transpose, unpack, bessel_jn (transformational) and // bessel_yn (transformational) @@ -1100,8 +1126,10 @@ Expr> FoldIntrinsicFunction( return Fold(context, Expr{ComplexConstructor{ToReal(context, std::move(re)), ToReal(context, std::move(im))}}); + } else if (name == "merge") { + return FoldMerge(context, std::move(funcRef)); } - // TODO: cshift, dot_product, eoshift, matmul, merge, pack, product, + // TODO: cshift, dot_product, eoshift, matmul, pack, product, // reduce, spread, sum, transfer, transpose, unpack return Expr{std::move(funcRef)}; } @@ -1172,9 +1200,11 @@ Expr> FoldIntrinsicFunction( [&fptr](const Scalar &i, const Scalar &j) { return Scalar{std::invoke(fptr, i, j)}; })); + } else if (name == "merge") { + return FoldMerge(context, std::move(funcRef)); } // TODO: btest, cshift, dot_product, eoshift, is_iostat_end, - // is_iostat_eor, lge, lgt, lle, llt, logical, matmul, merge, out_of_range, + // is_iostat_eor, lge, lgt, lle, llt, logical, matmul, out_of_range, // pack, parity, reduce, spread, transfer, transpose, unpack return Expr{std::move(funcRef)}; } @@ -1201,12 +1231,14 @@ Expr> FoldIntrinsicFunction( context, std::move(funcRef), CharacterUtils::ADJUSTR); } else if (name == "max") { return FoldMINorMAX(context, std::move(funcRef), Ordering::Greater); + } else if (name == "merge") { + return FoldMerge(context, std::move(funcRef)); } else if (name == "min") { return FoldMINorMAX(context, std::move(funcRef), Ordering::Less); } else if (name == "new_line") { return Expr{Constant{CharacterUtils::NEW_LINE()}}; } - // TODO: cshift, eoshift, max, maxval, merge, min, minval, pack, reduce, + // TODO: cshift, eoshift, maxval, minval, pack, reduce, // repeat, spread, transfer, transpose, trim, unpack return Expr{std::move(funcRef)}; } @@ -1643,10 +1675,7 @@ private: Fold(context_, Expr{iDo.stride()})}; std::optional start{ToInt64(lower)}, end{ToInt64(upper)}, step{ToInt64(stride)}; - if (start && end && step) { - if (*step == 0) { - return false; - } + if (start && end && step && *step != 0) { bool result{true}; ConstantSubscript &j{context_.StartImpliedDo(iDo.name(), *start)}; if (*step > 0) { diff --git a/flang/lib/semantics/expression.cc b/flang/lib/semantics/expression.cc index 90a06a3292e5..9d32f98fedf0 100644 --- a/flang/lib/semantics/expression.cc +++ b/flang/lib/semantics/expression.cc @@ -1058,31 +1058,83 @@ int ExpressionAnalyzer::IntegerTypeSpecKind( // Array constructors -class ArrayConstructorContext : private ExpressionAnalyzer { +// Inverts a collection of generic ArrayConstructorValues that +// all happen to have the same actual type T into one ArrayConstructor. +template +ArrayConstructorValues MakeSpecific( + ArrayConstructorValues &&from) { + ArrayConstructorValues to; + for (ArrayConstructorValue &x : from) { + std::visit( + common::visitors{ + [&](common::CopyableIndirection> &&expr) { + auto *typed{UnwrapExpr>(expr.value())}; + to.Push(std::move(DEREF(typed))); + }, + [&](ImpliedDo &&impliedDo) { + to.Push(ImpliedDo{impliedDo.name(), + std::move(impliedDo.lower()), std::move(impliedDo.upper()), + std::move(impliedDo.stride()), + MakeSpecific(std::move(impliedDo.values()))}); + }, + }, + std::move(x.u)); + } + return to; +} + +class ArrayConstructorContext { public: ArrayConstructorContext( - ExpressionAnalyzer &c, std::optional &t) - : ExpressionAnalyzer{c}, type_{t} {} - ArrayConstructorContext(ArrayConstructorContext &) = default; - void Push(MaybeExpr &&); - void Add(const parser::AcValue &); - std::optional &type() const { return type_; } - const ArrayConstructorValues &values() { return values_; } + ExpressionAnalyzer &c, std::optional &&t) + : exprAnalyzer_{c}, type_{std::move(t)} {} -private: - template - std::optional>> GetSpecificIntExpr( - const A &x) { - if (MaybeExpr y{Analyze(x)}) { - Expr *intExpr{UnwrapExpr>(*y)}; - CHECK(intExpr); - return ConvertToType>( - std::move(*intExpr)); + void Add(const parser::AcValue &); + MaybeExpr ToExpr(); + + // These interfaces allow *this to be used as a type visitor argument to + // common::SearchTypes() to convert the array constructor to a typed + // expression in ToExpr(). + using Result = MaybeExpr; + using Types = AllTypes; + template Result Test() { + if (type_ && type_->category() == T::category) { + if constexpr (T::category == TypeCategory::Derived) { + return AsMaybeExpr(ArrayConstructor{ + type_->GetDerivedTypeSpec(), MakeSpecific(std::move(values_))}); + } else if (type_->kind() == T::kind) { + if constexpr (T::category == TypeCategory::Character) { + if (auto len{type_->LEN()}) { + return AsMaybeExpr(ArrayConstructor{ + *std::move(len), MakeSpecific(std::move(values_))}); + } + } else { + return AsMaybeExpr( + ArrayConstructor{MakeSpecific(std::move(values_))}); + } + } } return std::nullopt; } - std::optional &type_; +private: + void Push(MaybeExpr &&); + + template + std::optional>> GetSpecificIntExpr( + const A &x) { + if (MaybeExpr y{exprAnalyzer_.Analyze(x)}) { + Expr *intExpr{UnwrapExpr>(*y)}; + return ConvertToType>( + std::move(DEREF(intExpr))); + } + return std::nullopt; + } + + // Nested array constructors all reference the same ExpressionAnalyzer, + // which represents the nest of active implied DO loop indices. + ExpressionAnalyzer &exprAnalyzer_; + std::optional type_; bool explicitType_{type_.has_value()}; std::optional constantLength_; ArrayConstructorValues values_; @@ -1117,9 +1169,10 @@ void ArrayConstructorContext::Push(MaybeExpr &&x) { values_.Push(std::move(*x)); if (auto thisLen{ToInt64(xType.LEN())}) { if (constantLength_) { - if (context().warnOnNonstandardUsage() && + if (exprAnalyzer_.context().warnOnNonstandardUsage() && *thisLen != *constantLength_) { - Say("Character literal in array constructor without explicit " + exprAnalyzer_.Say( + "Character literal in array constructor without explicit " "type has different length than earlier element"_en_US); } if (*thisLen > *constantLength_) { @@ -1135,14 +1188,16 @@ void ArrayConstructorContext::Push(MaybeExpr &&x) { } } } else { - Say("Values in array constructor must have the same declared type " + exprAnalyzer_.Say( + "Values in array constructor must have the same declared type " "when no explicit type appears"_err_en_US); } } else { if (auto cast{ConvertToType(*type_, std::move(*x))}) { values_.Push(std::move(*cast)); } else { - Say("Value in array constructor could not be converted to the type " + exprAnalyzer_.Say( + "Value in array constructor could not be converted to the type " "of the array"_err_en_US); } } @@ -1168,19 +1223,19 @@ void ArrayConstructorContext::Add(const parser::AcValue &x) { if (!type_) { type_ = DynamicTypeWithLength{IntType::GetType()}; } - ArrayConstructorContext nested{*this}; - parser::CharBlock name; - nested.Push(Expr{ - Expr{Expr{ImpliedDoIndex{name}}}}); - values_.Push(ImpliedDo{name, std::move(*lower), - std::move(*upper), std::move(*stride), - std::move(nested.values_)}); + auto v{std::move(values_)}; + parser::CharBlock anonymous; + Push(Expr{ + Expr{Expr{ImpliedDoIndex{anonymous}}}}); + std::swap(v, values_); + values_.Push(ImpliedDo{anonymous, std::move(*lower), + std::move(*upper), std::move(*stride), std::move(v)}); } }, [&](const common::Indirection &expr) { - auto restorer{ - GetContextualMessages().SetLocation(expr.value().source)}; - if (MaybeExpr v{Analyze(expr.value())}) { + auto restorer{exprAnalyzer_.GetContextualMessages().SetLocation( + expr.value().source)}; + if (MaybeExpr v{exprAnalyzer_.Analyze(expr.value())}) { Push(std::move(*v)); } }, @@ -1189,111 +1244,55 @@ void ArrayConstructorContext::Add(const parser::AcValue &x) { std::get(impliedDo.value().t)}; const auto &bounds{ std::get(control.t)}; - Analyze(bounds.name); + exprAnalyzer_.Analyze(bounds.name); parser::CharBlock name{bounds.name.thing.thing.source}; const Symbol *symbol{bounds.name.thing.thing.symbol}; int kind{IntType::kind}; if (const auto dynamicType{DynamicType::From(symbol)}) { kind = dynamicType->kind(); } - bool inserted{AddAcImpliedDo(name, kind)}; - if (!inserted) { - SayAt(name, + if (exprAnalyzer_.AddAcImpliedDo(name, kind)) { + std::optional> lower{ + GetSpecificIntExpr(bounds.lower)}; + std::optional> upper{ + GetSpecificIntExpr(bounds.upper)}; + if (lower && upper) { + std::optional> stride{ + GetSpecificIntExpr(bounds.step)}; + auto v{std::move(values_)}; + for (const auto &value : + std::get>(impliedDo.value().t)) { + Add(value); + } + if (!stride) { + stride = Expr{1}; + } + std::swap(v, values_); + values_.Push(ImpliedDo{name, std::move(*lower), + std::move(*upper), std::move(*stride), std::move(v)}); + } + exprAnalyzer_.RemoveAcImpliedDo(name); + } else { + exprAnalyzer_.SayAt(name, "Implied DO index is active in surrounding implied DO loop " "and may not have the same name"_err_en_US); } - std::optional> lower{ - GetSpecificIntExpr(bounds.lower)}; - std::optional> upper{ - GetSpecificIntExpr(bounds.upper)}; - std::optional> stride{ - GetSpecificIntExpr(bounds.step)}; - ArrayConstructorContext nested{*this}; - for (const auto &value : - std::get>(impliedDo.value().t)) { - nested.Add(value); - } - if (lower && upper) { - if (!stride) { - stride = Expr{1}; - } - values_.Push(ImpliedDo{name, std::move(*lower), - std::move(*upper), std::move(*stride), - std::move(nested.values_)}); - } - if (inserted) { - RemoveAcImpliedDo(name); - } }, }, x.u); } -// Inverts a collection of generic ArrayConstructorValues that -// all happen to have the same actual type T into one ArrayConstructor. -template -ArrayConstructorValues MakeSpecific( - ArrayConstructorValues &&from) { - ArrayConstructorValues to; - for (ArrayConstructorValue &x : from) { - std::visit( - common::visitors{ - [&](common::CopyableIndirection> &&expr) { - auto *typed{UnwrapExpr>(expr.value())}; - CHECK(typed); - to.Push(std::move(*typed)); - }, - [&](ImpliedDo &&impliedDo) { - to.Push(ImpliedDo{impliedDo.name(), - std::move(impliedDo.lower()), std::move(impliedDo.upper()), - std::move(impliedDo.stride()), - MakeSpecific(std::move(impliedDo.values()))}); - }, - }, - std::move(x.u)); - } - return to; +MaybeExpr ArrayConstructorContext::ToExpr() { + return common::SearchTypes(std::move(*this)); } -struct ArrayConstructorTypeVisitor { - using Result = MaybeExpr; - using Types = AllTypes; - template Result Test() { - if (type.category() == T::category) { - if constexpr (T::category == TypeCategory::Derived) { - return AsMaybeExpr(ArrayConstructor{ - type.GetDerivedTypeSpec(), MakeSpecific(std::move(values))}); - } else if (type.kind() == T::kind) { - if constexpr (T::category == TypeCategory::Character) { - if (auto len{type.LEN()}) { - return AsMaybeExpr(ArrayConstructor{ - *std::move(len), MakeSpecific(std::move(values))}); - } - } else { - return AsMaybeExpr( - ArrayConstructor{MakeSpecific(std::move(values))}); - } - } - } - return std::nullopt; - } - DynamicTypeWithLength type; - ArrayConstructorValues values; -}; - MaybeExpr ExpressionAnalyzer::Analyze(const parser::ArrayConstructor &array) { const parser::AcSpec &acSpec{array.v}; - std::optional type{AnalyzeTypeSpec(acSpec.type)}; - ArrayConstructorContext context{*this, type}; + ArrayConstructorContext acContext{*this, AnalyzeTypeSpec(acSpec.type)}; for (const parser::AcValue &value : acSpec.values) { - context.Add(value); + acContext.Add(value); } - if (type) { - ArrayConstructorTypeVisitor visitor{ - std::move(*type), std::move(context.values())}; - return common::SearchTypes(std::move(visitor)); - } - return std::nullopt; + return acContext.ToExpr(); } MaybeExpr ExpressionAnalyzer::Analyze( diff --git a/flang/module/iso_fortran_env.f90 b/flang/module/iso_fortran_env.f90 index c15dd199f4f3..b47f8955330b 100644 --- a/flang/module/iso_fortran_env.f90 +++ b/flang/module/iso_fortran_env.f90 @@ -19,20 +19,113 @@ include '../runtime/magic-numbers.h' ! for IOSTAT= error/end code values module iso_fortran_env - integer, parameter :: atomic_int_kind = 8 - integer, parameter :: atomic_logical_kind = 8 + implicit none - integer, parameter :: character_kinds(*) = [1, 2, 4] - integer, parameter :: int8 = 1, int16 = 2, int32 = 4, int64 = 8, int128 = 16 - integer, parameter :: integer_kinds(*) = [int8, int16, int32, int64, int128] + integer, parameter :: atomic_int_kind = selected_int_kind(18) + integer, parameter :: atomic_logical_kind = atomic_int_kind + + ! TODO: Use PACK([x],test) in place of the array constructor idiom + ! [(x, integer::j=1,COUNT([test]))] below once PACK() can be folded. + + integer, parameter, private :: & + selectedASCII = selected_char_kind('ASCII'), & + selectedUCS_2 = selected_char_kind('UCS-2'), & + selectedUnicode = selected_char_kind('ISO_10646') + integer, parameter :: character_kinds(*) = [ & + [(selectedASCII, integer :: j=1, count([selectedASCII >= 0]))], & + [(selectedUCS_2, integer :: j=1, count([selectedUCS_2 >= 0]))], & + [(selectedUnicode, integer :: j=1, count([selectedUnicode >= 0]))]] + + integer, parameter, private :: & + selectedInt8 = selected_int_kind(2), & + selectedInt16 = selected_int_kind(4), & + selectedInt32 = selected_int_kind(9), & + selectedInt64 = selected_int_kind(18),& + selectedInt128 = selected_int_kind(38), & + safeInt8 = merge(selectedInt8, selected_int_kind(0), & + selectedInt8 >= 0), & + safeInt16 = merge(selectedInt16, selected_int_kind(0), & + selectedInt16 >= 0), & + safeInt32 = merge(selectedInt32, selected_int_kind(0), & + selectedInt32 >= 0), & + safeInt64 = merge(selectedInt64, selected_int_kind(0), & + selectedInt64 >= 0), & + safeInt128 = merge(selectedInt128, selected_int_kind(0), & + selectedInt128 >= 0) integer, parameter :: & - logical8 = 1, logical16 = 2, logical32 = 4, logical64 = 8 - integer, parameter :: logical_kinds(*) = & - [logical8, logical16, logical32, logical64] + int8 = merge(selectedInt8, merge(-2, -1, selectedInt8 >= 0), & + digits(int(0,kind=safeInt8)) == 7), & + int16 = merge(selectedInt16, merge(-2, -1, selectedInt16 >= 0), & + digits(int(0,kind=safeInt16)) == 15), & + int32 = merge(selectedInt32, merge(-2, -1, selectedInt32 >= 0), & + digits(int(0,kind=safeInt32)) == 31), & + int64 = merge(selectedInt64, merge(-2, -1, selectedInt64 >= 0), & + digits(int(0,kind=safeInt64)) == 63), & + int128 = merge(selectedInt128, merge(-2, -1, selectedInt128 >= 0), & + digits(int(0,kind=safeInt128)) == 127) + + integer, parameter :: integer_kinds(*) = [ & + selected_int_kind(0), & + ((selected_int_kind(k), & + integer :: j=1, count([selected_int_kind(k) >= 0 .and. & + selected_int_kind(k) /= & + selected_int_kind(k-1)])), & + integer :: k=1, 39)] + integer, parameter :: & - real16 = 2, real32 = 4, real64 = 8, real80 = 10, real128 = 16 - integer, parameter :: real_kinds(*) = & - [real16, 3, real32, real64, real80, real128] + logical8 = int8, logical16 = int16, logical32 = int32, logical64 = int64 + integer, parameter :: logical_kinds(*) = [ & + [(logical8, integer :: j=1, count([logical8 >= 0]))], & + [(logical16, integer :: j=1, count([logical16 >= 0]))], & + [(logical32, integer :: j=1, count([logical32 >= 0]))], & + [(logical64, integer :: j=1, count([logical64 >= 0]))]] + + integer, parameter, private :: & + selectedReal16 = selected_real_kind(3, 4), & ! IEEE half + selectedBfloat16 = selected_real_kind(2, 37), & ! truncated IEEE single + selectedReal32 = selected_real_kind(6, 37), & ! IEEE single + selectedReal64 = selected_real_kind(15, 307), & ! IEEE double + selectedReal80 = selected_real_kind(18, 4931), & ! 80x87 extended + selectedReal64x2 = selected_real_kind(31, 307), & ! "double-double" + selectedReal128 = selected_real_kind(33, 9863), & ! IEEE quad + safeReal16 = merge(selectedReal16, selected_real_kind(0,0), & + selectedReal16 >= 0), & + safeBfloat16 = merge(selectedBfloat16, selected_real_kind(0,0), & + selectedBfloat16 >= 0), & + safeReal32 = merge(selectedReal32, selected_real_kind(0,0), & + selectedReal32 >= 0), & + safeReal64 = merge(selectedReal64, selected_real_kind(0,0), & + selectedReal64 >= 0), & + safeReal80 = merge(selectedReal80, selected_real_kind(0,0), & + selectedReal80 >= 0), & + safeReal64x2 = merge(selectedReal64x2, selected_real_kind(0,0), & + selectedReal64x2 >= 0), & + safeReal128 = merge(selectedReal128, selected_real_kind(0,0), & + selectedReal128 >= 0) + integer, parameter :: & + real16 = merge(selectedReal16, merge(-2, -1, selectedReal16 >= 0), & + digits(real(0,kind=safeReal16)) == 11), & + bfloat16 = merge(selectedBfloat16, merge(-2, -1, selectedBfloat16 >= 0), & + digits(real(0,kind=safeBfloat16)) == 8), & + real32 = merge(selectedReal32, merge(-2, -1, selectedReal32 >= 0), & + digits(real(0,kind=safeReal32)) == 24), & + real64 = merge(selectedReal64, merge(-2, -1, selectedReal64 >= 0), & + digits(real(0,kind=safeReal64)) == 53), & + real80 = merge(selectedReal80, merge(-2, -1, selectedReal80 >= 0), & + digits(real(0,kind=safeReal80)) == 64), & + real64x2 = merge(selectedReal64x2, merge(-2, -1, selectedReal64x2 >= 0), & + digits(real(0,kind=safeReal64x2)) == 106), & + real128 = merge(selectedReal128, merge(-2, -1, selectedReal128 >= 0), & + digits(real(0,kind=safeReal128)) == 112) + + integer, parameter :: real_kinds(*) = [ & + [(real16, integer :: j=1, count([real16 >= 0]))], & + [(bfloat16, integer :: j=1, count([bfloat16 >= 0]))], & + [(real32, integer :: j=1, count([real32 >= 0]))], & + [(real64, integer :: j=1, count([real64 >= 0]))], & + [(real80, integer :: j=1, count([real80 >= 0]))], & + [(real64x2, integer :: j=1, count([real64x2 >= 0]))], & + [(real128, integer :: j=1, count([real128 >= 0]))]] integer, parameter :: current_team = -1, initial_team = -2, parent_team = -3 @@ -76,4 +169,3 @@ module iso_fortran_env compiler_version = 'f18 in development' end function compiler_version end module iso_fortran_env -