Skip to content

Commit 0ca1447

Browse files
authored
[flang][OpenMP] Add optional argument to requirement clauses (#163557)
OpenMP 6.0 added an optional logical parameter to the requirement clauses (except ATOMIC_DEFAULT_MEM_ORDER) to indicate whether the clause should take effect or not. The parameter defaults to true if not specified. The parameter value is a compile-time constant expression, but it may require folding to get the final value. Since name resolution happens before folding, the argument expression needs to be analyzed by hand. The determination of the value needs to happen during name resolution because the requirement directives need to be available through module files (and the module reader doesn't to semantic checks beyond name resolution).
1 parent e6afe2a commit 0ca1447

File tree

12 files changed

+263
-50
lines changed

12 files changed

+263
-50
lines changed

flang/include/flang/Lower/OpenMP/Clauses.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ using Replayable = tomp::clause::ReplayableT<TypeTy, IdTy, ExprTy>;
282282
using ReverseOffload = tomp::clause::ReverseOffloadT<TypeTy, IdTy, ExprTy>;
283283
using Safelen = tomp::clause::SafelenT<TypeTy, IdTy, ExprTy>;
284284
using Schedule = tomp::clause::ScheduleT<TypeTy, IdTy, ExprTy>;
285+
using SelfMaps = tomp::clause::SelfMapsT<TypeTy, IdTy, ExprTy>;
285286
using SeqCst = tomp::clause::SeqCstT<TypeTy, IdTy, ExprTy>;
286287
using Severity = tomp::clause::SeverityT<TypeTy, IdTy, ExprTy>;
287288
using Shared = tomp::clause::SharedT<TypeTy, IdTy, ExprTy>;

flang/include/flang/Parser/dump-parse-tree.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,7 @@ class ParseTreeDumper {
568568
NODE(OmpDoacross, Sink)
569569
NODE(OmpDoacross, Source)
570570
NODE(parser, OmpDoacrossClause)
571+
NODE(parser, OmpDynamicAllocatorsClause)
571572
NODE(parser, OmpDynGroupprivateClause)
572573
NODE(OmpDynGroupprivateClause, Modifier)
573574
NODE(parser, OmpEndDirective)
@@ -661,9 +662,11 @@ class ParseTreeDumper {
661662
NODE(parser, OmpRefModifier)
662663
NODE_ENUM(OmpRefModifier, Value)
663664
NODE(parser, OmpReplayableClause)
665+
NODE(parser, OmpReverseOffloadClause)
664666
NODE(parser, OmpScheduleClause)
665667
NODE(OmpScheduleClause, Modifier)
666668
NODE_ENUM(OmpScheduleClause, Kind)
669+
NODE(parser, OmpSelfMapsClause)
667670
NODE(parser, OmpSelfModifier)
668671
NODE_ENUM(OmpSelfModifier, Value)
669672
NODE(parser, OmpSeverityClause)
@@ -691,6 +694,8 @@ class ParseTreeDumper {
691694
NODE(parser, OmpTransparentClause)
692695
NODE(parser, OmpTypeNameList)
693696
NODE(parser, OmpTypeSpecifier)
697+
NODE(parser, OmpUnifiedAddressClause)
698+
NODE(parser, OmpUnifiedSharedMemoryClause)
694699
NODE(parser, OmpUpdateClause)
695700
NODE(parser, OmpUseClause)
696701
NODE(parser, OmpVariableCategory)

flang/include/flang/Parser/parse-tree.h

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ using IntConstantExpr = Integer<ConstantExpr>; // R1031
337337
using ScalarLogicalExpr = Scalar<LogicalExpr>;
338338
using ScalarIntExpr = Scalar<IntExpr>;
339339
using ScalarIntConstantExpr = Scalar<IntConstantExpr>;
340+
using ScalarLogicalConstantExpr = Scalar<Logical<ConstantExpr>>;
340341
using ScalarDefaultCharExpr = Scalar<DefaultCharExpr>;
341342
// R1030 default-char-constant-expr is used in the Standard only as part of
342343
// scalar-default-char-constant-expr.
@@ -4426,6 +4427,16 @@ struct OmpDeviceTypeClause {
44264427
WRAPPER_CLASS_BOILERPLATE(OmpDeviceTypeClause, DeviceTypeDescription);
44274428
};
44284429

4430+
// Ref: [5.0:60-63], [5.1:83-86], [5.2:212-213], [6.0:356-362]
4431+
//
4432+
// dynamic-allocators-clause ->
4433+
// DYNAMIC_ALLOCATORS // since 5.0
4434+
// [(scalar-logical-const-expr)] // since 6.0
4435+
struct OmpDynamicAllocatorsClause {
4436+
WRAPPER_CLASS_BOILERPLATE(
4437+
OmpDynamicAllocatorsClause, ScalarLogicalConstantExpr);
4438+
};
4439+
44294440
struct OmpDynGroupprivateClause {
44304441
TUPLE_CLASS_BOILERPLATE(OmpDynGroupprivateClause);
44314442
MODIFIER_BOILERPLATE(OmpAccessGroup, OmpPrescriptiveness);
@@ -4703,7 +4714,16 @@ struct OmpReductionClause {
47034714
// replayable-clause ->
47044715
// REPLAYABLE[(replayable-expression)] // since 6.0
47054716
struct OmpReplayableClause {
4706-
WRAPPER_CLASS_BOILERPLATE(OmpReplayableClause, Scalar<Logical<ConstantExpr>>);
4717+
WRAPPER_CLASS_BOILERPLATE(OmpReplayableClause, ScalarLogicalConstantExpr);
4718+
};
4719+
4720+
// Ref: [5.0:60-63], [5.1:83-86], [5.2:212-213], [6.0:356-362]
4721+
//
4722+
// reverse-offload-clause ->
4723+
// REVERSE_OFFLOAD // since 5.0
4724+
// [(scalar-logical-const-expr)] // since 6.0
4725+
struct OmpReverseOffloadClause {
4726+
WRAPPER_CLASS_BOILERPLATE(OmpReverseOffloadClause, ScalarLogicalConstantExpr);
47074727
};
47084728

47094729
// Ref: [4.5:56-63], [5.0:101-109], [5.1:126-133], [5.2:252-254]
@@ -4721,6 +4741,14 @@ struct OmpScheduleClause {
47214741
std::tuple<MODIFIERS(), Kind, std::optional<ScalarIntExpr>> t;
47224742
};
47234743

4744+
// ref: [6.0:361-362]
4745+
//
4746+
// self-maps-clause ->
4747+
// SELF_MAPS [(scalar-logical-const-expr)] // since 6.0
4748+
struct OmpSelfMapsClause {
4749+
WRAPPER_CLASS_BOILERPLATE(OmpSelfMapsClause, ScalarLogicalConstantExpr);
4750+
};
4751+
47244752
// REF: [5.2:217]
47254753
// severity-clause ->
47264754
// SEVERITY(warning|fatal)
@@ -4763,6 +4791,25 @@ struct OmpTransparentClause {
47634791
WRAPPER_CLASS_BOILERPLATE(OmpTransparentClause, ScalarIntExpr);
47644792
};
47654793

4794+
// Ref: [5.0:60-63], [5.1:83-86], [5.2:212-213], [6.0:356-362]
4795+
//
4796+
// unified-address-clause ->
4797+
// UNIFIED_ADDRESS // since 5.0
4798+
// [(scalar-logical-const-expr)] // since 6.0
4799+
struct OmpUnifiedAddressClause {
4800+
WRAPPER_CLASS_BOILERPLATE(OmpUnifiedAddressClause, ScalarLogicalConstantExpr);
4801+
};
4802+
4803+
// Ref: [5.0:60-63], [5.1:83-86], [5.2:212-213], [6.0:356-362]
4804+
//
4805+
// unified-shared-memory-clause ->
4806+
// UNIFIED_SHARED_MEMORY // since 5.0
4807+
// [(scalar-logical-const-expr)] // since 6.0
4808+
struct OmpUnifiedSharedMemoryClause {
4809+
WRAPPER_CLASS_BOILERPLATE(
4810+
OmpUnifiedSharedMemoryClause, ScalarLogicalConstantExpr);
4811+
};
4812+
47664813
// Ref: [5.0:254-255], [5.1:287-288], [5.2:321-322]
47674814
//
47684815
// In ATOMIC construct

flang/lib/Lower/OpenMP/Clauses.cpp

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ MAKE_EMPTY_CLASS(AcqRel, AcqRel);
219219
MAKE_EMPTY_CLASS(Acquire, Acquire);
220220
MAKE_EMPTY_CLASS(Capture, Capture);
221221
MAKE_EMPTY_CLASS(Compare, Compare);
222-
MAKE_EMPTY_CLASS(DynamicAllocators, DynamicAllocators);
223222
MAKE_EMPTY_CLASS(Full, Full);
224223
MAKE_EMPTY_CLASS(Inbranch, Inbranch);
225224
MAKE_EMPTY_CLASS(Mergeable, Mergeable);
@@ -235,13 +234,9 @@ MAKE_EMPTY_CLASS(OmpxBare, OmpxBare);
235234
MAKE_EMPTY_CLASS(Read, Read);
236235
MAKE_EMPTY_CLASS(Relaxed, Relaxed);
237236
MAKE_EMPTY_CLASS(Release, Release);
238-
MAKE_EMPTY_CLASS(ReverseOffload, ReverseOffload);
239237
MAKE_EMPTY_CLASS(SeqCst, SeqCst);
240-
MAKE_EMPTY_CLASS(SelfMaps, SelfMaps);
241238
MAKE_EMPTY_CLASS(Simd, Simd);
242239
MAKE_EMPTY_CLASS(Threads, Threads);
243-
MAKE_EMPTY_CLASS(UnifiedAddress, UnifiedAddress);
244-
MAKE_EMPTY_CLASS(UnifiedSharedMemory, UnifiedSharedMemory);
245240
MAKE_EMPTY_CLASS(Unknown, Unknown);
246241
MAKE_EMPTY_CLASS(Untied, Untied);
247242
MAKE_EMPTY_CLASS(Weak, Weak);
@@ -775,7 +770,18 @@ Doacross make(const parser::OmpClause::Doacross &inp,
775770
return makeDoacross(inp.v.v, semaCtx);
776771
}
777772

778-
// DynamicAllocators: empty
773+
DynamicAllocators make(const parser::OmpClause::DynamicAllocators &inp,
774+
semantics::SemanticsContext &semaCtx) {
775+
// inp.v -> td::optional<arser::OmpDynamicAllocatorsClause>
776+
auto &&maybeRequired = maybeApply(
777+
[&](const parser::OmpDynamicAllocatorsClause &c) {
778+
return makeExpr(c.v, semaCtx);
779+
},
780+
inp.v);
781+
782+
return DynamicAllocators{/*Required=*/std::move(maybeRequired)};
783+
}
784+
779785

780786
DynGroupprivate make(const parser::OmpClause::DynGroupprivate &inp,
781787
semantics::SemanticsContext &semaCtx) {
@@ -1338,7 +1344,18 @@ Reduction make(const parser::OmpClause::Reduction &inp,
13381344

13391345
// Relaxed: empty
13401346
// Release: empty
1341-
// ReverseOffload: empty
1347+
1348+
ReverseOffload make(const parser::OmpClause::ReverseOffload &inp,
1349+
semantics::SemanticsContext &semaCtx) {
1350+
// inp.v -> std::optional<parser::OmpReverseOffloadClause>
1351+
auto &&maybeRequired = maybeApply(
1352+
[&](const parser::OmpReverseOffloadClause &c) {
1353+
return makeExpr(c.v, semaCtx);
1354+
},
1355+
inp.v);
1356+
1357+
return ReverseOffload{/*Required=*/std::move(maybeRequired)};
1358+
}
13421359

13431360
Safelen make(const parser::OmpClause::Safelen &inp,
13441361
semantics::SemanticsContext &semaCtx) {
@@ -1391,6 +1408,18 @@ Schedule make(const parser::OmpClause::Schedule &inp,
13911408

13921409
// SeqCst: empty
13931410

1411+
SelfMaps make(const parser::OmpClause::SelfMaps &inp,
1412+
semantics::SemanticsContext &semaCtx) {
1413+
// inp.v -> std::optional<parser::OmpSelfMapsClause>
1414+
auto &&maybeRequired = maybeApply(
1415+
[&](const parser::OmpSelfMapsClause &c) {
1416+
return makeExpr(c.v, semaCtx);
1417+
},
1418+
inp.v);
1419+
1420+
return SelfMaps{/*Required=*/std::move(maybeRequired)};
1421+
}
1422+
13941423
Severity make(const parser::OmpClause::Severity &inp,
13951424
semantics::SemanticsContext &semaCtx) {
13961425
// inp -> empty
@@ -1480,8 +1509,29 @@ To make(const parser::OmpClause::To &inp,
14801509
/*LocatorList=*/makeObjects(t3, semaCtx)}};
14811510
}
14821511

1483-
// UnifiedAddress: empty
1484-
// UnifiedSharedMemory: empty
1512+
UnifiedAddress make(const parser::OmpClause::UnifiedAddress &inp,
1513+
semantics::SemanticsContext &semaCtx) {
1514+
// inp.v -> std::optional<parser::OmpUnifiedAddressClause>
1515+
auto &&maybeRequired = maybeApply(
1516+
[&](const parser::OmpUnifiedAddressClause &c) {
1517+
return makeExpr(c.v, semaCtx);
1518+
},
1519+
inp.v);
1520+
1521+
return UnifiedAddress{/*Required=*/std::move(maybeRequired)};
1522+
}
1523+
1524+
UnifiedSharedMemory make(const parser::OmpClause::UnifiedSharedMemory &inp,
1525+
semantics::SemanticsContext &semaCtx) {
1526+
// inp.v -> std::optional<parser::OmpUnifiedSharedMemoryClause>
1527+
auto &&maybeRequired = maybeApply(
1528+
[&](const parser::OmpUnifiedSharedMemoryClause &c) {
1529+
return makeExpr(c.v, semaCtx);
1530+
},
1531+
inp.v);
1532+
1533+
return UnifiedSharedMemory{/*Required=*/std::move(maybeRequired)};
1534+
}
14851535

14861536
Uniform make(const parser::OmpClause::Uniform &inp,
14871537
semantics::SemanticsContext &semaCtx) {

flang/lib/Parser/openmp-parsers.cpp

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,7 +1167,8 @@ TYPE_PARSER( //
11671167
"DOACROSS" >>
11681168
construct<OmpClause>(parenthesized(Parser<OmpDoacrossClause>{})) ||
11691169
"DYNAMIC_ALLOCATORS" >>
1170-
construct<OmpClause>(construct<OmpClause::DynamicAllocators>()) ||
1170+
construct<OmpClause>(construct<OmpClause::DynamicAllocators>(
1171+
maybe(parenthesized(scalarLogicalConstantExpr)))) ||
11711172
"DYN_GROUPPRIVATE" >>
11721173
construct<OmpClause>(construct<OmpClause::DynGroupprivate>(
11731174
parenthesized(Parser<OmpDynGroupprivateClause>{}))) ||
@@ -1279,12 +1280,15 @@ TYPE_PARSER( //
12791280
"REPLAYABLE" >> construct<OmpClause>(construct<OmpClause::Replayable>(
12801281
maybe(parenthesized(Parser<OmpReplayableClause>{})))) ||
12811282
"REVERSE_OFFLOAD" >>
1282-
construct<OmpClause>(construct<OmpClause::ReverseOffload>()) ||
1283+
construct<OmpClause>(construct<OmpClause::ReverseOffload>(
1284+
maybe(parenthesized(scalarLogicalConstantExpr)))) ||
12831285
"SAFELEN" >> construct<OmpClause>(construct<OmpClause::Safelen>(
12841286
parenthesized(scalarIntConstantExpr))) ||
12851287
"SCHEDULE" >> construct<OmpClause>(construct<OmpClause::Schedule>(
12861288
parenthesized(Parser<OmpScheduleClause>{}))) ||
12871289
"SEQ_CST" >> construct<OmpClause>(construct<OmpClause::SeqCst>()) ||
1290+
"SELF_MAPS" >> construct<OmpClause>(construct<OmpClause::SelfMaps>(
1291+
maybe(parenthesized(scalarLogicalConstantExpr)))) ||
12881292
"SEVERITY" >> construct<OmpClause>(construct<OmpClause::Severity>(
12891293
parenthesized(Parser<OmpSeverityClause>{}))) ||
12901294
"SHARED" >> construct<OmpClause>(construct<OmpClause::Shared>(
@@ -1312,9 +1316,11 @@ TYPE_PARSER( //
13121316
construct<OmpClause>(construct<OmpClause::UseDeviceAddr>(
13131317
parenthesized(Parser<OmpObjectList>{}))) ||
13141318
"UNIFIED_ADDRESS" >>
1315-
construct<OmpClause>(construct<OmpClause::UnifiedAddress>()) ||
1319+
construct<OmpClause>(construct<OmpClause::UnifiedAddress>(
1320+
maybe(parenthesized(scalarLogicalConstantExpr)))) ||
13161321
"UNIFIED_SHARED_MEMORY" >>
1317-
construct<OmpClause>(construct<OmpClause::UnifiedSharedMemory>()) ||
1322+
construct<OmpClause>(construct<OmpClause::UnifiedSharedMemory>(
1323+
maybe(parenthesized(scalarLogicalConstantExpr)))) ||
13181324
"UNIFORM" >> construct<OmpClause>(construct<OmpClause::Uniform>(
13191325
parenthesized(nonemptyList(name)))) ||
13201326
"UNTIED" >> construct<OmpClause>(construct<OmpClause::Untied>()) ||

flang/lib/Semantics/check-omp-structure.cpp

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,19 +1517,42 @@ void OmpStructureChecker::Leave(const parser::OpenMPDepobjConstruct &x) {
15171517
void OmpStructureChecker::Enter(const parser::OpenMPRequiresConstruct &x) {
15181518
const auto &dirName{x.v.DirName()};
15191519
PushContextAndClauseSets(dirName.source, dirName.v);
1520+
unsigned version{context_.langOptions().OpenMPVersion};
15201521

1521-
if (visitedAtomicSource_.empty()) {
1522-
return;
1523-
}
15241522
for (const parser::OmpClause &clause : x.v.Clauses().v) {
15251523
llvm::omp::Clause id{clause.Id()};
15261524
if (id == llvm::omp::Clause::OMPC_atomic_default_mem_order) {
1527-
parser::MessageFormattedText txt(
1528-
"REQUIRES directive with '%s' clause found lexically after atomic operation without a memory order clause"_err_en_US,
1529-
parser::ToUpperCaseLetters(llvm::omp::getOpenMPClauseName(id)));
1530-
parser::Message message(clause.source, txt);
1531-
message.Attach(visitedAtomicSource_, "Previous atomic construct"_en_US);
1532-
context_.Say(std::move(message));
1525+
if (!visitedAtomicSource_.empty()) {
1526+
parser::MessageFormattedText txt(
1527+
"REQUIRES directive with '%s' clause found lexically after atomic operation without a memory order clause"_err_en_US,
1528+
parser::ToUpperCaseLetters(llvm::omp::getOpenMPClauseName(id)));
1529+
parser::Message message(clause.source, txt);
1530+
message.Attach(visitedAtomicSource_, "Previous atomic construct"_en_US);
1531+
context_.Say(std::move(message));
1532+
}
1533+
} else {
1534+
bool hasArgument{common::visit(
1535+
[&](auto &&s) {
1536+
using TypeS = llvm::remove_cvref_t<decltype(s)>;
1537+
if constexpr ( //
1538+
std::is_same_v<TypeS, parser::OmpClause::DynamicAllocators> ||
1539+
std::is_same_v<TypeS, parser::OmpClause::ReverseOffload> ||
1540+
std::is_same_v<TypeS, parser::OmpClause::SelfMaps> ||
1541+
std::is_same_v<TypeS, parser::OmpClause::UnifiedAddress> ||
1542+
std::is_same_v<TypeS, parser::OmpClause::UnifiedSharedMemory>) {
1543+
return s.v.has_value();
1544+
} else {
1545+
return false;
1546+
}
1547+
},
1548+
clause.u)};
1549+
if (version < 60 && hasArgument) {
1550+
context_.Say(clause.source,
1551+
"An argument to %s is an %s feature, %s"_warn_en_US,
1552+
parser::ToUpperCaseLetters(
1553+
llvm::omp::getOpenMPClauseName(clause.Id())),
1554+
ThisVersion(60), TryVersion(60));
1555+
}
15331556
}
15341557
}
15351558
}

flang/lib/Semantics/resolve-directives.cpp

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,21 @@ class OmpAttributeVisitor : DirectiveAttributeVisitor<llvm::omp::Directive> {
557557
using RequiresClauses = WithOmpDeclarative::RequiresClauses;
558558
PushContext(x.source, llvm::omp::Directive::OMPD_requires);
559559

560+
auto getArgument{[&](auto &&maybeClause) {
561+
if (maybeClause) {
562+
// Scalar<Logical<Constant<common::Indirection<Expr>>>>
563+
auto &parserExpr{maybeClause->v.thing.thing.thing.value()};
564+
evaluate::ExpressionAnalyzer ea{context_};
565+
if (auto &&maybeExpr{ea.Analyze(parserExpr)}) {
566+
if (auto v{omp::GetLogicalValue(*maybeExpr)}) {
567+
return *v;
568+
}
569+
}
570+
}
571+
// If the argument is missing, it is assumed to be true.
572+
return true;
573+
}};
574+
560575
// Gather information from the clauses.
561576
RequiresClauses reqs;
562577
const common::OmpMemoryOrderType *memOrder{nullptr};
@@ -573,16 +588,19 @@ class OmpAttributeVisitor : DirectiveAttributeVisitor<llvm::omp::Directive> {
573588
if constexpr ( //
574589
std::is_same_v<TypeS, OmpClause::DynamicAllocators> ||
575590
std::is_same_v<TypeS, OmpClause::ReverseOffload> ||
591+
std::is_same_v<TypeS, OmpClause::SelfMaps> ||
576592
std::is_same_v<TypeS, OmpClause::UnifiedAddress> ||
577593
std::is_same_v<TypeS, OmpClause::UnifiedSharedMemory>) {
578-
return RequiresClauses{clause.Id()};
579-
} else {
580-
return RequiresClauses{};
594+
if (getArgument(s.v)) {
595+
return RequiresClauses{clause.Id()};
596+
}
581597
}
598+
return RequiresClauses{};
582599
},
583600
},
584601
clause.u);
585602
}
603+
586604
// Merge clauses into parents' symbols details.
587605
AddOmpRequiresToScope(currScope(), &reqs, memOrder);
588606
return true;

flang/test/Parser/OpenMP/requires.f90

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,18 @@
3030
!PARSE-TREE: | OmpClause -> ReverseOffload
3131
!PARSE-TREE: | Flags = None
3232

33+
!$omp requires self_maps(.true.) unified_address(.false.)
34+
35+
!UNPARSE: !$OMP REQUIRES SELF_MAPS(.true._4) UNIFIED_ADDRESS(.false._4)
36+
37+
!PARSE-TREE: OpenMPDeclarativeConstruct -> OpenMPRequiresConstruct -> OmpDirectiveSpecification
38+
!PARSE-TREE: | OmpDirectiveName -> llvm::omp::Directive = requires
39+
!PARSE-TREE: | OmpClauseList -> OmpClause -> SelfMaps -> OmpSelfMapsClause -> Scalar -> Logical -> Constant -> Expr = '.true._4'
40+
!PARSE-TREE: | | LiteralConstant -> LogicalLiteralConstant
41+
!PARSE-TREE: | | | bool = 'true'
42+
!PARSE-TREE: | OmpClause -> UnifiedAddress -> OmpUnifiedAddressClause -> Scalar -> Logical -> Constant -> Expr = '.false._4'
43+
!PARSE-TREE: | | LiteralConstant -> LogicalLiteralConstant
44+
!PARSE-TREE: | | | bool = 'false'
45+
!PARSE-TREE: | Flags = None
46+
3347
end

0 commit comments

Comments
 (0)