Skip to content

Commit 7b6ec6c

Browse files
author
David Ungar
authored
Merge pull request #16271 from davidungar/rdar-39805050-Switch-takes-too-long-to-compile
Add cutoff to limit time taking to check exhaustiveness of switch statement.
2 parents 7fa2016 + 037a2c8 commit 7b6ec6c

File tree

8 files changed

+120
-45
lines changed

8 files changed

+120
-45
lines changed

include/swift/Frontend/FrontendOptions.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ class FrontendOptions {
8989
/// the expression type checker run before we consider an expression
9090
/// too complex.
9191
unsigned SolverExpressionTimeThreshold = 0;
92+
93+
/// If non-zero, overrides the default threshold for how many times
94+
/// the Space::minus function is called before we consider switch statement
95+
/// exhaustiveness checking to be too complex.
96+
unsigned SwitchCheckingInvocationThreshold = 0;
9297

9398
/// The module for which we should verify all of the generic signatures.
9499
std::string VerifyGenericSignaturesInModule;

include/swift/Option/FrontendOptions.td

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,9 @@ def warn_long_expression_type_checking_EQ : Joined<["-"], "warn-long-expression-
345345

346346
def solver_expression_time_threshold_EQ : Joined<["-"], "solver-expression-time-threshold=">;
347347

348+
def switch_checking_invocation_threshold_EQ : Joined<["-"],
349+
"switch-checking-invocation-threshold=">;
350+
348351
def enable_source_import : Flag<["-"], "enable-source-import">,
349352
HelpText<"Enable importing of Swift source files">;
350353

include/swift/Subsystems.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ namespace swift {
189189
unsigned StartElem = 0,
190190
unsigned WarnLongFunctionBodies = 0,
191191
unsigned WarnLongExpressionTypeChecking = 0,
192-
unsigned ExpressionTimeoutThreshold = 0);
192+
unsigned ExpressionTimeoutThreshold = 0,
193+
unsigned SwitchCheckingInvocationThreshold = 0);
193194

194195
/// Now that we have type-checked an entire module, perform any type
195196
/// checking that requires the full module, e.g., Objective-C method

lib/Frontend/ArgsToFrontendOptionsConverter.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ bool ArgsToFrontendOptionsConverter::convert(
7878
Opts.WarnLongExpressionTypeChecking);
7979
setUnsignedIntegerArgument(OPT_solver_expression_time_threshold_EQ, 10,
8080
Opts.SolverExpressionTimeThreshold);
81+
setUnsignedIntegerArgument(OPT_switch_checking_invocation_threshold_EQ, 10,
82+
Opts.SwitchCheckingInvocationThreshold);
8183

8284
Opts.DebuggerTestingTransform = Args.hasArg(OPT_debugger_testing_transform);
8385

lib/Frontend/Frontend.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,8 @@ void CompilerInstance::parseAndCheckTypes(
628628
TypeCheckOptions, /*curElem*/ 0,
629629
options.WarnLongFunctionBodies,
630630
options.WarnLongExpressionTypeChecking,
631-
options.SolverExpressionTimeThreshold);
631+
options.SolverExpressionTimeThreshold,
632+
options.SwitchCheckingInvocationThreshold);
632633
});
633634

634635
// Even if there were no source files, we should still record known
@@ -751,7 +752,8 @@ void CompilerInstance::parseAndTypeCheckMainFile(
751752
TypeCheckOptions, CurTUElem,
752753
options.WarnLongFunctionBodies,
753754
options.WarnLongExpressionTypeChecking,
754-
options.SolverExpressionTimeThreshold);
755+
options.SolverExpressionTimeThreshold,
756+
options.SwitchCheckingInvocationThreshold);
755757
}
756758
CurTUElem = MainFile.Decls.size();
757759
} while (!Done);

lib/Sema/TypeCheckSwitchStmt.cpp

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -686,9 +686,19 @@ namespace {
686686
// Returns the result of subtracting the other space from this space. The
687687
// result is empty if the other space completely covers this space, or
688688
// non-empty if there were any uncovered cases. The difference of spaces
689-
// is the smallest uncovered set of cases.
690-
Space minus(const Space &other, TypeChecker &TC,
691-
const DeclContext *DC) const {
689+
// is the smallest uncovered set of cases. The result is absent if the
690+
// computation had to be abandoned.
691+
//
692+
// \p minusCount is an optional pointer counting the number of
693+
// times minus has run.
694+
// Returns None if the computation "timed out".
695+
Optional<Space> minus(const Space &other, TypeChecker &TC,
696+
const DeclContext *DC, unsigned *minusCount) const {
697+
698+
if (minusCount && TC.getSwitchCheckingInvocationThreshold() &&
699+
(*minusCount)++ >= TC.getSwitchCheckingInvocationThreshold())
700+
return None;
701+
692702
if (this->isEmpty()) {
693703
return Space();
694704
}
@@ -719,7 +729,7 @@ namespace {
719729
SmallVector<Space, 4> spaces;
720730
this->decompose(TC, DC, this->getType(), spaces);
721731
auto decomp = Space::forDisjunct(spaces);
722-
return decomp.minus(other, TC, DC);
732+
return decomp.minus(other, TC, DC, minusCount);
723733
} else {
724734
return *this;
725735
}
@@ -738,12 +748,14 @@ namespace {
738748
PAIRCASE (SpaceKind::Disjunct, SpaceKind::Disjunct):
739749
PAIRCASE (SpaceKind::BooleanConstant, SpaceKind::Disjunct):
740750
PAIRCASE (SpaceKind::UnknownCase, SpaceKind::Disjunct): {
741-
return std::accumulate(other.getSpaces().begin(),
742-
other.getSpaces().end(),
743-
*this,
744-
[&](const Space &left, const Space &right){
745-
return left.minus(right, TC, DC);
746-
});
751+
Space tot = *this;
752+
for (auto s : other.getSpaces()) {
753+
if (auto diff = tot.minus(s, TC, DC, minusCount))
754+
tot = *diff;
755+
else
756+
return None;
757+
}
758+
return tot;
747759
}
748760

749761
PAIRCASE (SpaceKind::Disjunct, SpaceKind::Empty):
@@ -752,19 +764,23 @@ namespace {
752764
PAIRCASE (SpaceKind::Disjunct, SpaceKind::BooleanConstant):
753765
PAIRCASE (SpaceKind::Disjunct, SpaceKind::UnknownCase): {
754766
SmallVector<Space, 4> smallSpaces;
755-
std::transform(this->getSpaces().begin(), this->getSpaces().end(),
756-
std::back_inserter(smallSpaces),
757-
[&](const Space &first){
758-
return first.minus(other, TC, DC);
759-
});
767+
for (auto s : this->getSpaces()) {
768+
if (auto diff = s.minus(other, TC, DC, minusCount))
769+
smallSpaces.push_back(*diff);
770+
else
771+
return None;
772+
}
760773
return Space::forDisjunct(smallSpaces);
761774
}
762775
PAIRCASE (SpaceKind::Constructor, SpaceKind::Type):
763776
return Space();
764777
PAIRCASE (SpaceKind::Constructor, SpaceKind::UnknownCase): {
765778
SmallVector<Space, 4> newSubSpaces;
766779
for (auto subSpace : this->getSpaces()) {
767-
Space nextSpace = subSpace.minus(other, TC, DC).simplify(TC, DC);
780+
auto diff = subSpace.minus(other, TC, DC, minusCount);
781+
if (!diff)
782+
return None;
783+
auto nextSpace = diff->simplify(TC, DC);
768784
if (nextSpace.isEmpty())
769785
return Space();
770786
newSubSpaces.push_back(nextSpace);
@@ -813,7 +829,11 @@ namespace {
813829
SmallVector<Space, 4> copyParams(this->getSpaces().begin(),
814830
this->getSpaces().end());
815831

816-
auto reducedSpace = s1.minus(s2, TC, DC);
832+
auto reducedSpaceOrNone = s1.minus(s2, TC, DC, minusCount);
833+
if (!reducedSpaceOrNone)
834+
return None;
835+
auto reducedSpace = *reducedSpaceOrNone;
836+
817837
// If one of the constructor parameters is empty it means
818838
// the whole constructor space is empty as well, so we can
819839
// safely skip it.
@@ -857,7 +877,7 @@ namespace {
857877
SmallVector<Space, 4> spaces;
858878
this->decompose(TC, DC, other.getType(), spaces);
859879
auto disjunctSp = Space::forDisjunct(spaces);
860-
return this->minus(disjunctSp, TC, DC);
880+
return this->minus(disjunctSp, TC, DC, minusCount);
861881
}
862882
return *this;
863883
}
@@ -871,7 +891,7 @@ namespace {
871891
SmallVector<Space, 4> spaces;
872892
this->decompose(TC, DC, this->getType(), spaces);
873893
auto orSpace = Space::forDisjunct(spaces);
874-
return orSpace.minus(other, TC, DC);
894+
return orSpace.minus(other, TC, DC, minusCount);
875895
} else {
876896
return *this;
877897
}
@@ -1286,26 +1306,21 @@ namespace {
12861306
Space totalSpace = Space::forType(subjectType, Identifier());
12871307
Space coveredSpace = Space::forDisjunct(spaces);
12881308

1289-
size_t totalSpaceSize = totalSpace.getSize(TC, DC);
1309+
const size_t totalSpaceSize = totalSpace.getSize(TC, DC);
12901310
if (totalSpaceSize > Space::getMaximumSize()) {
1291-
// Because the space is large, we have to extend the size
1292-
// heuristic to compensate for actually exhaustively pattern matching
1293-
// over enormous spaces. In this case, if the covered space covers
1294-
// as much as the total space, and there were no duplicates, then we
1295-
// can assume the user did the right thing and that they don't need
1296-
// a 'default' to be inserted.
1297-
// FIXME: Do something sensible for non-frozen enums.
1298-
if (!sawRedundantPattern
1299-
&& coveredSpace.getSize(TC, DC) >= totalSpaceSize) {
1300-
return;
1301-
}
1302-
1303-
diagnoseMissingCases(RequiresDefault::SpaceTooLarge, Space(),
1304-
unknownCase);
1311+
diagnoseCannotCheck(sawRedundantPattern, totalSpaceSize, coveredSpace,
1312+
unknownCase);
13051313
return;
13061314
}
1307-
1308-
auto uncovered = totalSpace.minus(coveredSpace, TC, DC).simplify(TC, DC);
1315+
unsigned minusCount = 0;
1316+
auto diff = totalSpace.minus(coveredSpace, TC, DC, &minusCount);
1317+
if (!diff) {
1318+
diagnoseCannotCheck(sawRedundantPattern, totalSpaceSize, coveredSpace,
1319+
unknownCase);
1320+
return;
1321+
}
1322+
1323+
auto uncovered = diff->simplify(TC, DC);
13091324
if (unknownCase && uncovered.isEmpty()) {
13101325
TC.diagnose(unknownCase->getLoc(), diag::redundant_particular_case)
13111326
.highlight(unknownCase->getSourceRange());
@@ -1314,8 +1329,8 @@ namespace {
13141329
// Account for unknown cases. If the developer wrote `unknown`, they're
13151330
// all handled; otherwise, we ignore the ones that were added for enums
13161331
// that are implicitly frozen.
1317-
uncovered = uncovered.minus(Space::forUnknown(unknownCase == nullptr),
1318-
TC, DC);
1332+
uncovered = *uncovered.minus(Space::forUnknown(unknownCase == nullptr),
1333+
TC, DC, /*&minusCount*/ nullptr);
13191334
uncovered = uncovered.simplify(TC, DC);
13201335

13211336
if (uncovered.isEmpty())
@@ -1350,9 +1365,27 @@ namespace {
13501365
UncoveredSwitch,
13511366
SpaceTooLarge,
13521367
};
1353-
1354-
void diagnoseMissingCases(const RequiresDefault defaultReason,
1355-
Space uncovered,
1368+
1369+
void diagnoseCannotCheck(const bool sawRedundantPattern,
1370+
const size_t totalSpaceSize,
1371+
const Space &coveredSpace,
1372+
const CaseStmt *unknownCase) {
1373+
// Because the space is large or the check is too slow,
1374+
// we have to extend the size
1375+
// heuristic to compensate for actually exhaustively pattern matching
1376+
// over enormous spaces. In this case, if the covered space covers
1377+
// as much as the total space, and there were no duplicates, then we
1378+
// can assume the user did the right thing and that they don't need
1379+
// a 'default' to be inserted.
1380+
// FIXME: Do something sensible for non-frozen enums.
1381+
if (!sawRedundantPattern &&
1382+
coveredSpace.getSize(TC, DC) >= totalSpaceSize)
1383+
return;
1384+
diagnoseMissingCases(RequiresDefault::SpaceTooLarge, Space(),
1385+
unknownCase);
1386+
}
1387+
1388+
void diagnoseMissingCases(RequiresDefault defaultReason, Space uncovered,
13561389
const CaseStmt *unknownCase = nullptr,
13571390
bool sawDowngradablePattern = false) {
13581391
SourceLoc startLoc = Switch->getStartLoc();

lib/Sema/TypeChecker.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,8 @@ void swift::performTypeChecking(SourceFile &SF, TopLevelContext &TLC,
576576
unsigned StartElem,
577577
unsigned WarnLongFunctionBodies,
578578
unsigned WarnLongExpressionTypeChecking,
579-
unsigned ExpressionTimeoutThreshold) {
579+
unsigned ExpressionTimeoutThreshold,
580+
unsigned SwitchCheckingInvocationThreshold) {
580581
if (SF.ASTStage == SourceFile::TypeChecked)
581582
return;
582583

@@ -608,6 +609,10 @@ void swift::performTypeChecking(SourceFile &SF, TopLevelContext &TLC,
608609
if (ExpressionTimeoutThreshold != 0)
609610
MyTC->setExpressionTimeoutThreshold(ExpressionTimeoutThreshold);
610611

612+
if (SwitchCheckingInvocationThreshold != 0)
613+
MyTC->setSwitchCheckingInvocationThreshold(
614+
SwitchCheckingInvocationThreshold);
615+
611616
if (Options.contains(TypeCheckingFlags::DebugTimeFunctionBodies))
612617
MyTC->enableDebugTimeFunctionBodies();
613618

lib/Sema/TypeChecker.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,14 @@ class TypeChecker final : public LazyResolver {
922922
/// than this many seconds.
923923
unsigned ExpressionTimeoutThreshold = 600;
924924

925+
/// If non-zero, abort the switch statement exhaustiveness checker if
926+
/// the Space::minus function is called more than this many times.
927+
///
928+
/// Why this number? Times out in about a second on a 2017 iMac, Retina 5K,
929+
// 4.2 GHz Intel Core i7.
930+
// (It's arbitrary, but will keep the compiler from taking too much time.)
931+
unsigned SwitchCheckingInvocationThreshold = 200000;
932+
925933
/// If true, the time it takes to type-check each function will be dumped
926934
/// to llvm::errs().
927935
bool DebugTimeFunctionBodies = false;
@@ -997,10 +1005,26 @@ class TypeChecker final : public LazyResolver {
9971005
/// the upper bound for the number of seconds we'll let the
9981006
/// expression type checker run before considering an expression
9991007
/// "too complex".
1008+
/// If zero, do not limit the checking.
10001009
unsigned getExpressionTimeoutThresholdInSeconds() {
10011010
return ExpressionTimeoutThreshold;
10021011
}
10031012

1013+
/// Get the threshold that determines the upper bound for the number
1014+
/// of times we'll let the Space::minus routine run before
1015+
/// considering a switch statement "too complex".
1016+
/// If zero, do not limit the checking.
1017+
unsigned getSwitchCheckingInvocationThreshold() const {
1018+
return SwitchCheckingInvocationThreshold;
1019+
}
1020+
1021+
/// Set the threshold that determines the upper bound for the number
1022+
/// of times we'll let the Space::minus routine run before
1023+
/// considering a switch statement "too complex".
1024+
void setSwitchCheckingInvocationThreshold(unsigned invocationCount) {
1025+
SwitchCheckingInvocationThreshold = invocationCount;
1026+
}
1027+
10041028
bool getInImmediateMode() {
10051029
return InImmediateMode;
10061030
}

0 commit comments

Comments
 (0)