Skip to content

Commit bbbe082

Browse files
committed
Sema: Fix conformance checking for 'rethrows' protocol requirements
A protocol requirement in a non-'@rethrows' protocol cannot be witnessed by a rethrows-via-conformance witness, because there is not enough information at a potential call site of the protocol requirement to determine if the witness uses a rethrows source or not.
1 parent 8ae1b76 commit bbbe082

File tree

4 files changed

+57
-17
lines changed

4 files changed

+57
-17
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,6 +2232,9 @@ NOTE(protocol_witness_settable_conflict,none,
22322232
"candidate is not settable, but protocol requires it", ())
22332233
NOTE(protocol_witness_rethrows_conflict,none,
22342234
"candidate is not 'rethrows', but protocol requires it", ())
2235+
NOTE(protocol_witness_rethrows_by_conformance_conflict,none,
2236+
"candidate is 'rethrows' via a conformance, "
2237+
"but the protocol requirement is not from a '@rethrows' protocol", ())
22352238
NOTE(protocol_witness_throws_conflict,none,
22362239
"candidate throws, but protocol does not allow it", ())
22372240
NOTE(protocol_witness_not_objc,none,

lib/Sema/TypeCheckProtocol.cpp

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -558,25 +558,39 @@ swift::matchWitness(
558558
if (!funcReq->isMutating() && funcWitness->isMutating())
559559
return RequirementMatch(witness, MatchKind::MutatingConflict);
560560

561-
// If the requirement is rethrows, the witness must either be
562-
// rethrows or be non-throwing if the requirement is not by conformance
563-
// else the witness can be by conformance, throwing or non throwing
564-
if (reqAttrs.hasAttribute<RethrowsAttr>() &&
565-
!witnessAttrs.hasAttribute<RethrowsAttr>()) {
561+
// If the requirement has an explicit 'rethrows' argument, the witness
562+
// must be 'rethrows', too.
563+
if (reqAttrs.hasAttribute<RethrowsAttr>()) {
566564
auto reqRethrowingKind =
567565
funcReq->getPolymorphicEffectKind(EffectKind::Throws);
568566
auto witnessRethrowingKind =
569567
funcWitness->getPolymorphicEffectKind(EffectKind::Throws);
570-
if (reqRethrowingKind == PolymorphicEffectKind::ByConformance) {
571-
switch (witnessRethrowingKind) {
572-
case PolymorphicEffectKind::ByConformance:
573-
case PolymorphicEffectKind::Always:
574-
case PolymorphicEffectKind::None:
568+
569+
assert(reqRethrowingKind != PolymorphicEffectKind::Always &&
570+
reqRethrowingKind != PolymorphicEffectKind::None);
571+
572+
switch (witnessRethrowingKind) {
573+
case PolymorphicEffectKind::None:
574+
case PolymorphicEffectKind::Invalid:
575+
case PolymorphicEffectKind::ByClosure:
576+
break;
577+
578+
case PolymorphicEffectKind::ByConformance: {
579+
// A by-conformance `rethrows` witness cannot witness a
580+
// by-conformance `rethrows` requirement unless the protocol
581+
// is @rethrows. Otherwise, we don't have enough information
582+
// at the call site to assess if the conformance actually
583+
// throws or not.
584+
auto *proto = cast<ProtocolDecl>(req->getDeclContext());
585+
if (reqRethrowingKind == PolymorphicEffectKind::ByConformance &&
586+
proto->hasPolymorphicEffect(EffectKind::Throws))
575587
break;
576-
default:
577-
return RequirementMatch(witness, MatchKind::RethrowsConflict);
578-
}
579-
} else if (cast<AbstractFunctionDecl>(witness)->hasThrows()) {
588+
589+
return RequirementMatch(witness,
590+
MatchKind::RethrowsByConformanceConflict);
591+
}
592+
593+
case PolymorphicEffectKind::Always:
580594
return RequirementMatch(witness, MatchKind::RethrowsConflict);
581595
}
582596
}
@@ -2525,6 +2539,13 @@ diagnoseMatch(ModuleDecl *module, NormalProtocolConformance *conformance,
25252539
diag.fixItReplace(FD->getThrowsLoc(), getTokenText(tok::kw_rethrows));
25262540
break;
25272541
}
2542+
case MatchKind::RethrowsByConformanceConflict: {
2543+
auto witness = match.Witness;
2544+
auto diag =
2545+
diags.diagnose(witness,
2546+
diag::protocol_witness_rethrows_by_conformance_conflict);
2547+
break;
2548+
}
25282549
case MatchKind::NonObjC:
25292550
diags.diagnose(match.Witness, diag::protocol_witness_not_objc);
25302551
break;

lib/Sema/TypeCheckProtocol.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,13 @@ enum class MatchKind : uint8_t {
263263
/// The witness did not match because of __consuming conflicts.
264264
ConsumingConflict,
265265

266-
/// The witness is not rethrows, but the requirement is.
266+
/// The witness throws unconditionally, but the requirement rethrows.
267267
RethrowsConflict,
268268

269+
/// The witness rethrows via conformance, but the requirement rethrows
270+
/// via closure and is not in a '@rethrows' protocol.
271+
RethrowsByConformanceConflict,
272+
269273
/// The witness is explicitly @nonobjc but the requirement is @objc.
270274
NonObjC,
271275

@@ -498,6 +502,7 @@ struct RequirementMatch {
498502
case MatchKind::NonMutatingConflict:
499503
case MatchKind::ConsumingConflict:
500504
case MatchKind::RethrowsConflict:
505+
case MatchKind::RethrowsByConformanceConflict:
501506
case MatchKind::AsyncConflict:
502507
case MatchKind::ThrowsConflict:
503508
case MatchKind::NonObjC:
@@ -530,6 +535,7 @@ struct RequirementMatch {
530535
case MatchKind::NonMutatingConflict:
531536
case MatchKind::ConsumingConflict:
532537
case MatchKind::RethrowsConflict:
538+
case MatchKind::RethrowsByConformanceConflict:
533539
case MatchKind::AsyncConflict:
534540
case MatchKind::ThrowsConflict:
535541
case MatchKind::NonObjC:

test/attr/attr_rethrows_protocol.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,32 @@ func takesEmpty<T : Empty>(_: T) rethrows {}
139139

140140
takesEmpty(EmptyWitness())
141141

142-
// FIXME: Fix this soundness hole
142+
// Rethrows kinds need to line up when a 'rethrows' function witnesses a
143+
// 'rethrows' protocol requirement
143144

144-
// Note: SimpleThrowsClosure is not @rethrows
145+
// Note: the SimpleThrowsClosure protocol is not @rethrows
145146
protocol SimpleThrowsClosure {
146147
func doIt(_: () throws -> ()) rethrows
148+
// expected-note@-1 {{protocol requires function 'doIt' with type '(() throws -> ()) throws -> ()'; do you want to add a stub?}}
149+
150+
func doIt2<T : Empty>(_: T) rethrows
151+
// expected-note@-1 {{protocol requires function 'doIt2' with type '<T> (T) throws -> ()'; do you want to add a stub?}}
147152
}
148153

149154
struct ConformsToSimpleThrowsClosure<T : RethrowingProtocol> : SimpleThrowsClosure {
155+
// expected-error@-1 {{type 'ConformsToSimpleThrowsClosure<T>' does not conform to protocol 'SimpleThrowsClosure'}}
150156
let t: T
151157

152158
// This cannot witness SimpleThrowsClosure.doIt(), because the
153159
// T : RethrowingProtocol conformance is a source here, but that
154160
// is not captured in the protocol's requirement signature.
155161
func doIt(_: () throws -> ()) rethrows {
162+
// expected-note@-1 {{candidate is 'rethrows' via a conformance, but the protocol requirement is not from a '@rethrows' protocol}}
156163
try t.source()
157164
}
165+
166+
func doIt2<T : Empty>(_: T) rethrows {}
167+
// expected-note@-1 {{candidate is 'rethrows' via a conformance, but the protocol requirement is not from a '@rethrows' protocol}}
158168
}
159169

160170
func soundnessHole<T : SimpleThrowsClosure>(_ t: T) {

0 commit comments

Comments
 (0)