[SILGen] Generate a trap for unexpected cases in all @objc enums

(both C enums and Swift enums declared @objc), because of the
"feature" in C of treating a value not declared as a case as a valid
value of an enum.  No more undefined behavior here!

This bit can go in separately from all the work on exhaustive/frozen
enums, which is still being discussed and will come later.

rdar://problem/20420436
This commit is contained in:
Jordan Rose
2018-02-19 17:12:31 -08:00
parent d782f467b0
commit 36eae9d4f6
6 changed files with 314 additions and 21 deletions

View File

@@ -1759,23 +1759,30 @@ static void generateEnumCaseBlocks(
assert(caseBBs.size() == caseInfos.size()); assert(caseBBs.size() == caseInfos.size());
// We always need a default block if the enum is resilient. // Check to see if the enum may have values beyond the cases we can see
// If the enum is @_fixed_layout, we only need one if the // at compile-time. This includes future cases (for resilient enums) and
// switch is not exhaustive. // random values crammed into C enums.
bool exhaustive = false; //
// Note: This relies on the fact that there are no "non-resilient" enums that
if (!enumDecl->isResilient(SGF.SGM.M.getSwiftModule(), // are still non-exhaustive, except for @objc enums.
SGF.F.getResilienceExpansion())) { bool canAssumeExhaustive = !enumDecl->isObjC();
exhaustive = true; if (canAssumeExhaustive) {
canAssumeExhaustive =
!enumDecl->isResilient(SGF.SGM.SwiftModule,
SGF.F.getResilienceExpansion());
}
if (canAssumeExhaustive) {
// Check that Sema didn't let any cases slip through. (This can happen
// with @_downgrade_exhaustivity_check.)
for (auto elt : enumDecl->getAllElements()) { for (auto elt : enumDecl->getAllElements()) {
if (!caseToIndex.count(elt)) { if (!caseToIndex.count(elt)) {
exhaustive = false; canAssumeExhaustive = false;
break; break;
} }
} }
} }
if (!exhaustive) if (!canAssumeExhaustive)
defaultBB = SGF.createBasicBlock(curBB); defaultBB = SGF.createBasicBlock(curBB);
} }
@@ -2542,17 +2549,21 @@ void SILGenFunction::emitSwitchStmt(SwitchStmt *S) {
DEBUG(llvm::dbgs() << "emitting switch stmt\n"; DEBUG(llvm::dbgs() << "emitting switch stmt\n";
S->print(llvm::dbgs()); S->print(llvm::dbgs());
llvm::dbgs() << '\n'); llvm::dbgs() << '\n');
auto failure = [&](SILLocation location) { auto failure = [this](SILLocation location) {
// If we fail to match anything, we can just emit unreachable. // If we fail to match anything, we trap. This can happen with a switch
// This will be a dataflow error if we can reach here. // over an @objc enum, which may contain any value of its underlying type.
B.createUnreachable(S); // FIXME: Even if we can't say what the invalid value was, we should at
// least mention that this was because of a non-exhaustive enum.
B.createBuiltinTrap(location);
B.createUnreachable(location);
}; };
// If the subject expression is uninhabited, we're already dead. // If the subject expression is uninhabited, we're already dead.
// Emit an unreachable in place of the switch statement. // Emit an unreachable in place of the switch statement.
if (S->getSubjectExpr()->getType()->isStructurallyUninhabited()) { if (S->getSubjectExpr()->getType()->isStructurallyUninhabited()) {
emitIgnoredExpr(S->getSubjectExpr()); emitIgnoredExpr(S->getSubjectExpr());
return failure(SILLocation(S)); B.createUnreachable(S);
return;
} }
auto completionHandler = [&](PatternMatchEmission &emission, auto completionHandler = [&](PatternMatchEmission &emission,

View File

@@ -16,9 +16,6 @@ func useFoo(_ x: Foo) -> Int32 {
// CHECK-DAG: i32 0, label %[[CASE_A:.+]] // CHECK-DAG: i32 0, label %[[CASE_A:.+]]
// CHECK: ] // CHECK: ]
// CHECK: <label>:[[DEFAULT]]
// CHECK-NEXT: unreachable
switch x { switch x {
// CHECK: <label>:[[CASE_B]] // CHECK: <label>:[[CASE_B]]
// CHECK-NEXT: br label %[[FINAL:.+]] // CHECK-NEXT: br label %[[FINAL:.+]]
@@ -36,6 +33,10 @@ func useFoo(_ x: Foo) -> Int32 {
return 10 return 10
} }
// CHECK: <label>:[[DEFAULT]]
// CHECK-NEXT: call void @llvm.trap()
// CHECK-NEXT: unreachable
// CHECK: <label>:[[FINAL]] // CHECK: <label>:[[FINAL]]
// CHECK: %[[RETVAL:.+]] = phi i32 [ 10, %[[CASE_A]] ], [ 15, %[[CASE_C]] ], [ 11, %[[CASE_B]] ] // CHECK: %[[RETVAL:.+]] = phi i32 [ 10, %[[CASE_A]] ], [ 15, %[[CASE_C]] ], [ 11, %[[CASE_B]] ]
// CHECK: ret i32 %[[RETVAL]] // CHECK: ret i32 %[[RETVAL]]
@@ -49,9 +50,6 @@ func useBar(_ x: Bar) -> Int32 {
// CHECK-DAG: i32 5, label %[[CASE_A:.+]] // CHECK-DAG: i32 5, label %[[CASE_A:.+]]
// CHECK: ] // CHECK: ]
// CHECK: <label>:[[DEFAULT]]
// CHECK-NEXT: unreachable
switch x { switch x {
// CHECK: <label>:[[CASE_B]] // CHECK: <label>:[[CASE_B]]
// CHECK-NEXT: br label %[[FINAL:.+]] // CHECK-NEXT: br label %[[FINAL:.+]]
@@ -69,6 +67,10 @@ func useBar(_ x: Bar) -> Int32 {
return 10 return 10
} }
// CHECK: <label>:[[DEFAULT]]
// CHECK-NEXT: call void @llvm.trap()
// CHECK-NEXT: unreachable
// CHECK: <label>:[[FINAL]] // CHECK: <label>:[[FINAL]]
// CHECK: %[[RETVAL:.+]] = phi i32 [ 10, %[[CASE_A]] ], [ 15, %[[CASE_C]] ], [ 11, %[[CASE_B]] ] // CHECK: %[[RETVAL:.+]] = phi i32 [ 10, %[[CASE_A]] ], [ 15, %[[CASE_C]] ], [ 11, %[[CASE_B]] ]
// CHECK: ret i32 %[[RETVAL]] // CHECK: ret i32 %[[RETVAL]]

View File

@@ -0,0 +1,25 @@
enum NonExhaustiveEnum {
NonExhaustiveEnumA = 0,
NonExhaustiveEnumB = 1,
NonExhaustiveEnumC = 2,
} __attribute__((enum_extensibility(open)));
enum NonExhaustiveEnum getExpectedValue(void) {
return NonExhaustiveEnumB;
}
enum NonExhaustiveEnum getUnexpectedValue(void) {
return (enum NonExhaustiveEnum)3;
}
enum LyingExhaustiveEnum {
LyingExhaustiveEnumA = 0,
LyingExhaustiveEnumB = 1,
LyingExhaustiveEnumC = 2,
} __attribute__((enum_extensibility(closed)));
enum LyingExhaustiveEnum getExpectedLiarValue(void) {
return LyingExhaustiveEnumB;
}
enum LyingExhaustiveEnum getUnexpectedLiarValue(void) {
return (enum LyingExhaustiveEnum)3;
}

View File

@@ -0,0 +1,250 @@
// RUN: %empty-directory(%t)
// RUN: %target-build-swift %s -Onone -o %t/main -import-objc-header %S/Inputs/enum-nonexhaustivity.h -Xfrontend -disable-objc-attr-requires-foundation-module
// RUN: %target-run %t/main
// RUN: %target-build-swift %s -O -o %t/main -import-objc-header %S/Inputs/enum-nonexhaustivity.h -Xfrontend -disable-objc-attr-requires-foundation-module
// RUN: %target-run %t/main
// RUN: %target-build-swift %s -Ounchecked -o %t/main -import-objc-header %S/Inputs/enum-nonexhaustivity.h -Xfrontend -disable-objc-attr-requires-foundation-module
// RUN: %target-run %t/main
// REQUIRES: executable_test
import StdlibUnittest
var EnumTestSuite = TestSuite("Enums")
EnumTestSuite.test("PlainOldSwitch/NonExhaustive") {
var gotCorrectValue = false
switch getExpectedValue() {
case .A, .C:
expectUnreachable()
case .B:
gotCorrectValue = true
}
expectTrue(gotCorrectValue)
}
EnumTestSuite.test("TrapOnUnexpected/NonExhaustive") {
expectCrashLater()
switch getUnexpectedValue() {
case .A, .C:
expectUnreachable()
case .B:
expectUnreachable()
}
expectUnreachable()
}
EnumTestSuite.test("TrapOnUnexpectedNested/NonExhaustive") {
expectCrashLater()
switch (getExpectedValue(), getUnexpectedValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (_, .B):
expectUnreachable()
case (_, .A), (_, .C):
expectUnreachable()
}
expectUnreachable()
}
EnumTestSuite.test("TrapOnUnexpectedNested2/NonExhaustive") {
expectCrashLater()
switch (getUnexpectedValue(), getExpectedValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (.B, _):
expectUnreachable()
case (.A, _), (.C, _):
expectUnreachable()
}
expectUnreachable()
}
EnumTestSuite.test("UnexpectedOkayNested/NonExhaustive") {
var gotCorrectValue = false
switch (getExpectedValue(), getUnexpectedValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (.B, _):
gotCorrectValue = true
case (.A, _), (.C, _):
expectUnreachable()
}
expectTrue(gotCorrectValue)
}
EnumTestSuite.test("UnexpectedOkayNested2/NonExhaustive") {
var gotCorrectValue = false
switch (getUnexpectedValue(), getExpectedValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (_, .B):
gotCorrectValue = true
case (_, .A), (_, .C):
expectUnreachable()
}
expectTrue(gotCorrectValue)
}
EnumTestSuite.test("PlainOldSwitch/LyingExhaustive") {
var gotCorrectValue = false
switch getExpectedLiarValue() {
case .A, .C:
expectUnreachable()
case .B:
gotCorrectValue = true
}
expectTrue(gotCorrectValue)
}
EnumTestSuite.test("TrapOnUnexpected/LyingExhaustive") {
expectCrashLater()
switch getUnexpectedLiarValue() {
case .A, .C:
expectUnreachable()
case .B:
expectUnreachable()
}
expectUnreachable()
}
EnumTestSuite.test("TrapOnUnexpectedNested/LyingExhaustive") {
expectCrashLater()
switch (getExpectedLiarValue(), getUnexpectedLiarValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (_, .B):
expectUnreachable()
case (_, .A), (_, .C):
expectUnreachable()
}
expectUnreachable()
}
EnumTestSuite.test("TrapOnUnexpectedNested2/LyingExhaustive") {
expectCrashLater()
switch (getUnexpectedLiarValue(), getExpectedLiarValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (.B, _):
expectUnreachable()
case (.A, _), (.C, _):
expectUnreachable()
}
expectUnreachable()
}
EnumTestSuite.test("UnexpectedOkayNested/LyingExhaustive") {
var gotCorrectValue = false
switch (getExpectedLiarValue(), getUnexpectedLiarValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (.B, _):
gotCorrectValue = true
case (.A, _), (.C, _):
expectUnreachable()
}
expectTrue(gotCorrectValue)
}
EnumTestSuite.test("UnexpectedOkayNested2/LyingExhaustive") {
var gotCorrectValue = false
switch (getUnexpectedLiarValue(), getExpectedLiarValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (_, .B):
gotCorrectValue = true
case (_, .A), (_, .C):
expectUnreachable()
}
expectTrue(gotCorrectValue)
}
@objc enum SwiftEnum : Int32 {
case A, B, C
@inline(never) static func getExpectedValue() -> SwiftEnum {
return .B
}
@inline(never) static func getUnexpectedValue() -> SwiftEnum {
return unsafeBitCast(42 as Int32, to: SwiftEnum.self)
}
}
EnumTestSuite.test("PlainOldSwitch/SwiftExhaustive") {
var gotCorrectValue = false
switch SwiftEnum.getExpectedValue() {
case .A, .C:
expectUnreachable()
case .B:
gotCorrectValue = true
}
expectTrue(gotCorrectValue)
}
EnumTestSuite.test("TrapOnUnexpected/SwiftExhaustive") {
expectCrashLater()
switch SwiftEnum.getUnexpectedValue() {
case .A, .C:
expectUnreachable()
case .B:
expectUnreachable()
}
expectUnreachable()
}
EnumTestSuite.test("TrapOnUnexpectedNested/SwiftExhaustive") {
expectCrashLater()
switch (SwiftEnum.getExpectedValue(), SwiftEnum.getUnexpectedValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (_, .B):
expectUnreachable()
case (_, .A), (_, .C):
expectUnreachable()
}
expectUnreachable()
}
EnumTestSuite.test("TrapOnUnexpectedNested2/SwiftExhaustive") {
expectCrashLater()
switch (SwiftEnum.getUnexpectedValue(), SwiftEnum.getExpectedValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (.B, _):
expectUnreachable()
case (.A, _), (.C, _):
expectUnreachable()
}
expectUnreachable()
}
EnumTestSuite.test("UnexpectedOkayNested/SwiftExhaustive") {
var gotCorrectValue = false
switch (SwiftEnum.getExpectedValue(), SwiftEnum.getUnexpectedValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (.B, _):
gotCorrectValue = true
case (.A, _), (.C, _):
expectUnreachable()
}
expectTrue(gotCorrectValue)
}
EnumTestSuite.test("UnexpectedOkayNested2/SwiftExhaustive") {
var gotCorrectValue = false
switch (SwiftEnum.getUnexpectedValue(), SwiftEnum.getExpectedValue()) {
case (.A, .A), (.C, .C):
expectUnreachable()
case (_, .B):
gotCorrectValue = true
case (_, .A), (_, .C):
expectUnreachable()
}
expectTrue(gotCorrectValue)
}
runAllTests()

View File

@@ -18,6 +18,7 @@ func testDowngradableOmittedPatternIsUnreachable(pat : Downgradable?) {
case .hat: case .hat:
break break
// CHECK: [[DEFAULT_CASE]]({{%.*}} : @trivial $Downgradable): // CHECK: [[DEFAULT_CASE]]({{%.*}} : @trivial $Downgradable):
// CHECK-NEXT: builtin "int_trap"()
// CHECK-NEXT: unreachable // CHECK-NEXT: unreachable
} }
@@ -33,14 +34,17 @@ func testDowngradableOmittedPatternIsUnreachable(pat : Downgradable?) {
case (.hat, .hat): case (.hat, .hat):
break break
// CHECK: [[TUPLE_DEFAULT_CASE_2]]({{%.*}} : @trivial $Downgradable): // CHECK: [[TUPLE_DEFAULT_CASE_2]]({{%.*}} : @trivial $Downgradable):
// CHECK-NEXT: builtin "int_trap"()
// CHECK-NEXT: unreachable // CHECK-NEXT: unreachable
// CHECK: switch_enum [[Y]] : $Downgradable, case #Downgradable.spoon!enumelt: {{bb[0-9]+}}, case #Downgradable.hat!enumelt: {{bb[0-9]+}}, default [[TUPLE_DEFAULT_CASE_3:bb[0-9]+]] // CHECK: switch_enum [[Y]] : $Downgradable, case #Downgradable.spoon!enumelt: {{bb[0-9]+}}, case #Downgradable.hat!enumelt: {{bb[0-9]+}}, default [[TUPLE_DEFAULT_CASE_3:bb[0-9]+]]
// CHECK: [[TUPLE_DEFAULT_CASE_3]]({{%.*}} : @trivial $Downgradable): // CHECK: [[TUPLE_DEFAULT_CASE_3]]({{%.*}} : @trivial $Downgradable):
// CHECK-NEXT: builtin "int_trap"()
// CHECK-NEXT: unreachable // CHECK-NEXT: unreachable
// CHECK: [[TUPLE_DEFAULT_CASE_1]]({{%.*}} : @trivial $Downgradable): // CHECK: [[TUPLE_DEFAULT_CASE_1]]({{%.*}} : @trivial $Downgradable):
// CHECK-NEXT: builtin "int_trap"()
// CHECK-NEXT: unreachable // CHECK-NEXT: unreachable
} }

View File

@@ -31,6 +31,7 @@ import resilient_enum
// CHECK-NEXT: dealloc_stack [[BOX]] // CHECK-NEXT: dealloc_stack [[BOX]]
// CHECK-NEXT: br bb6 // CHECK-NEXT: br bb6
// CHECK: bb5: // CHECK: bb5:
// CHECK-NEXT: builtin "int_trap"()
// CHECK-NEXT: unreachable // CHECK-NEXT: unreachable
// CHECK: bb6: // CHECK: bb6:
// CHECK-NEXT: destroy_addr %0 // CHECK-NEXT: destroy_addr %0