Skip to content

Commit efbb0f1

Browse files
committed
implement typechecking for effects specifiers on 'get' accessors
The basic rule is that the protocol requirement describes the maximal set of effects that the property is allowed to have. Thus, witnesses must have the same or fewer effects specifiers on the getter. For class inheritance overrides, you can remove effects freely, as long as you stay within the bounds of the normal override restrictions. But, you cannot override with more effects than the base member has. Same goes for protocol member overrides. Furthermore, we disallow key paths to effectful properties/subscripts, which also cannot be @objc.
1 parent 3702d0c commit efbb0f1

11 files changed

+404
-110
lines changed

include/swift/AST/Decl.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4521,6 +4521,34 @@ class AbstractStorageDecl : public ValueDecl {
45214521
return {};
45224522
}
45234523

4524+
/// This is the primary mechanism by which we can easily determine whether
4525+
/// this storage decl has any effects.
4526+
///
4527+
/// \returns the getter decl iff this decl has only one accessor that is
4528+
/// a 'get' with an effect (i.e., 'async', 'throws', or both).
4529+
/// Otherwise returns nullptr.
4530+
AccessorDecl *getEffectfulGetAccessor() const;
4531+
4532+
/// Performs a "limit check" on an effect possibly exhibited by this storage
4533+
/// decl with respect to some other storage decl that serves as the "limit."
4534+
/// This check says that \c this is less effectful than \c other if
4535+
/// \c this either does not exhibit the effect, or if it does, then \c other
4536+
/// also exhibits the effect. Thus, it is conceptually equivalent to
4537+
/// a less-than-or-equal (≤) check like so:
4538+
///
4539+
/// \verbatim
4540+
///
4541+
/// this->hasEffect(E) ≤ other->hasEffect(E)
4542+
///
4543+
/// \endverbatim
4544+
///
4545+
/// \param kind the single effect we are performing a check for.
4546+
///
4547+
/// \returns true iff \c this decl either does not exhibit the effect,
4548+
/// or \c other also exhibits the effect.
4549+
bool isLessEffectfulThan(AbstractStorageDecl const* other,
4550+
EffectKind kind) const;
4551+
45244552
/// Return an accessor that this storage is expected to have, synthesizing
45254553
/// one if necessary. Note that will always synthesize one, even if the
45264554
/// accessor is not part of the expected opaque set for the storage, so use

include/swift/AST/DiagnosticsSema.def

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,10 @@ WARNING(enum_frozen_nonpublic,none,
15841584
ERROR(getset_init,none,
15851585
"variable with getter/setter cannot have an initial value", ())
15861586

1587+
ERROR(effectful_not_representable_objc,none,
1588+
"%0 with 'throws' or 'async' is not representable in Objective-C",
1589+
(DescriptiveDeclKind))
1590+
15871591
ERROR(unimplemented_static_var,none,
15881592
"%select{ERROR|static|class}1 stored properties not supported"
15891593
"%select{ in this context| in generic types| in classes| in protocol extensions}0"
@@ -2600,6 +2604,10 @@ ERROR(override_class_declaration_in_extension,none,
26002604
ERROR(override_throws,none,
26012605
"cannot override non-throwing %select{method|initializer}0 with "
26022606
"throwing %select{method|initializer}0", (bool))
2607+
// TODO: this really could be merged with the above.
2608+
ERROR(override_with_more_effects,none,
2609+
"cannot override non-'%1' %0 with '%1' %0",
2610+
(DescriptiveDeclKind, StringRef))
26032611
ERROR(override_throws_objc,none,
26042612
"overriding a throwing @objc %select{method|initializer}0 with "
26052613
"a non-throwing %select{method|initializer}0 is not supported", (bool))
@@ -4150,21 +4158,21 @@ ERROR(isa_collection_downcast_pattern_value_unimplemented,none,
41504158
ERROR(try_unhandled,none,
41514159
"errors thrown from here are not handled", ())
41524160
ERROR(throwing_call_unhandled,none,
4153-
"call can throw, but the error is not handled", ())
4161+
"%0 can throw, but the error is not handled", (StringRef))
41544162
ERROR(tryless_throwing_call_unhandled,none,
4155-
"call can throw, but it is not marked with 'try' and "
4156-
"the error is not handled", ())
4163+
"%0 can throw, but it is not marked with 'try' and "
4164+
"the error is not handled", (StringRef))
41574165
ERROR(throw_in_nonthrowing_function,none,
41584166
"error is not handled because the enclosing function "
41594167
"is not declared 'throws'", ())
41604168

41614169
ERROR(throwing_call_in_rethrows_function,none,
4162-
"call can throw, but the error is not handled; a function declared "
4163-
"'rethrows' may only throw if its parameter does", ())
4170+
"%0 can throw, but the error is not handled; a function declared "
4171+
"'rethrows' may only throw if its parameter does", (StringRef))
41644172
ERROR(tryless_throwing_call_in_rethrows_function,none,
4165-
"call can throw, but it is not marked with 'try' and "
4173+
"%0 can throw, but it is not marked with 'try' and "
41664174
"the error is not handled; a function declared "
4167-
"'rethrows' may only throw if its parameter does", ())
4175+
"'rethrows' may only throw if its parameter does", (StringRef))
41684176
ERROR(throw_in_rethrows_function,none,
41694177
"a function declared 'rethrows' may only throw if its parameter does", ())
41704178
NOTE(because_rethrows_argument_throws,none,
@@ -4176,11 +4184,11 @@ NOTE(because_rethrows_conformance_throws,none,
41764184
"call is to 'rethrows' function, but a conformance has a throwing witness", ())
41774185

41784186
ERROR(throwing_call_in_nonthrowing_autoclosure,none,
4179-
"call can throw, but it is executed in a non-throwing "
4180-
"autoclosure",())
4187+
"%0 can throw, but it is executed in a non-throwing "
4188+
"autoclosure",(StringRef))
41814189
ERROR(tryless_throwing_call_in_nonthrowing_autoclosure,none,
4182-
"call can throw, but it is not marked with 'try' and "
4183-
"it is executed in a non-throwing autoclosure",())
4190+
"%0 can throw, but it is not marked with 'try' and "
4191+
"it is executed in a non-throwing autoclosure",(StringRef))
41844192
ERROR(throw_in_nonthrowing_autoclosure,none,
41854193
"error is not handled because it is thrown in a non-throwing "
41864194
"autoclosure", ())
@@ -4189,17 +4197,17 @@ ERROR(try_unhandled_in_nonexhaustive_catch,none,
41894197
"errors thrown from here are not handled because the "
41904198
"enclosing catch is not exhaustive", ())
41914199
ERROR(throwing_call_in_nonexhaustive_catch,none,
4192-
"call can throw, but the enclosing catch is not exhaustive", ())
4200+
"%0 can throw, but the enclosing catch is not exhaustive", (StringRef))
41934201
ERROR(tryless_throwing_call_in_nonexhaustive_catch,none,
4194-
"call can throw, but it is not marked with 'try' and "
4195-
"the enclosing catch is not exhaustive", ())
4202+
"%0 can throw, but it is not marked with 'try' and "
4203+
"the enclosing catch is not exhaustive", (StringRef))
41964204
ERROR(throw_in_nonexhaustive_catch,none,
41974205
"error is not handled because the enclosing catch is not exhaustive", ())
41984206

4199-
ERROR(throwing_call_in_illegal_context,none,
4200-
"call can throw, but errors cannot be thrown out of "
4207+
ERROR(throwing_op_in_illegal_context,none,
4208+
"%1 can throw, but errors cannot be thrown out of "
42014209
"%select{<<ERROR>>|a default argument|a property wrapper initializer|a property initializer|a global variable initializer|an enum case raw value|a catch pattern|a catch guard expression|a defer body}0",
4202-
(unsigned))
4210+
(unsigned, StringRef))
42034211
ERROR(throw_in_illegal_context,none,
42044212
"errors cannot be thrown out of "
42054213
"%select{<<ERROR>>|a default argument|a property wrapper initializer|a property initializer|a global variable initializer|an enum case raw value|a catch pattern|a catch guard expression|a defer body}0",
@@ -4213,6 +4221,10 @@ ERROR(throwing_call_without_try,none,
42134221
"call can throw but is not marked with 'try'", ())
42144222
ERROR(throwing_async_let_without_try,none,
42154223
"reading 'async let' can throw but is not marked with 'try'", ())
4224+
ERROR(throwing_prop_access_without_try,none,
4225+
"property access can throw but is not marked with 'try'", ())
4226+
ERROR(throwing_subscript_access_without_try,none,
4227+
"subscript access can throw but is not marked with 'try'", ())
42164228
NOTE(note_forgot_try,none,
42174229
"did you mean to use 'try'?", ())
42184230
NOTE(note_error_to_optional,none,
@@ -4382,6 +4394,9 @@ ERROR(actor_isolated_from_escaping_closure,none,
43824394
ERROR(actor_isolated_keypath_component,none,
43834395
"cannot form key path to actor-isolated %0 %1",
43844396
(DescriptiveDeclKind, DeclName))
4397+
ERROR(effectful_keypath_component,none,
4398+
"cannot form key path to %0 with 'throws' or 'async'",
4399+
(DescriptiveDeclKind))
43854400
ERROR(local_function_executed_concurrently,none,
43864401
"concurrently-executed %0 %1 must be marked as '@Sendable'",
43874402
(DescriptiveDeclKind, DeclName))
@@ -5615,6 +5630,9 @@ ERROR(property_wrapper_let, none,
56155630
ERROR(property_wrapper_computed, none,
56165631
"property wrapper cannot be applied to a computed property",
56175632
())
5633+
ERROR(property_wrapper_effectful,none,
5634+
"property wrappers currently cannot define an 'async' or 'throws' accessor",
5635+
())
56185636

56195637
ERROR(property_with_wrapper_conflict_attribute,none,
56205638
"property %0 with a wrapper cannot also be "

lib/AST/Decl.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2122,6 +2122,30 @@ AccessorDecl *AbstractStorageDecl::getSynthesizedAccessor(AccessorKind kind) con
21222122
nullptr);
21232123
}
21242124

2125+
AccessorDecl *AbstractStorageDecl::getEffectfulGetAccessor() const {
2126+
if (getAllAccessors().size() != 1)
2127+
return nullptr;
2128+
2129+
if (auto accessor = getAccessor(AccessorKind::Get))
2130+
if (accessor->hasAsync() || accessor->hasThrows())
2131+
return accessor;
2132+
2133+
return nullptr;
2134+
}
2135+
2136+
bool AbstractStorageDecl::isLessEffectfulThan(AbstractStorageDecl const* other,
2137+
EffectKind kind) const {
2138+
bool allowedByOther = false;
2139+
if (auto otherGetter = other->getEffectfulGetAccessor())
2140+
allowedByOther = otherGetter->hasEffect(kind);
2141+
2142+
if (auto getter = getEffectfulGetAccessor())
2143+
if (getter->hasEffect(kind) && !allowedByOther)
2144+
return false; // has the effect when other does not; it's more effectful!
2145+
2146+
return true; // OK
2147+
}
2148+
21252149
AccessorDecl *AbstractStorageDecl::getOpaqueAccessor(AccessorKind kind) const {
21262150
auto *accessor = getAccessor(kind);
21272151
if (accessor && !accessor->isImplicit())

lib/Sema/MiscDiagnostics.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ static Expr *isImplicitPromotionToOptional(Expr *E) {
6868
/// - Error about collection literals that default to Any collections in
6969
/// invalid positions.
7070
/// - Marker protocols cannot occur as the type of an as? or is expression.
71+
/// - KeyPath expressions cannot refer to effectful properties / subscripts
7172
///
7273
static void diagSyntacticUseRestrictions(const Expr *E, const DeclContext *DC,
7374
bool isExprStmt) {
@@ -150,6 +151,9 @@ static void diagSyntacticUseRestrictions(const Expr *E, const DeclContext *DC,
150151
CallArgs.insert(DSE->getIndex());
151152

152153
if (auto *KPE = dyn_cast<KeyPathExpr>(E)) {
154+
// raise an error if this KeyPath contains an effectful member.
155+
checkForEffectfulKeyPath(KPE);
156+
153157
for (auto Comp : KPE->getComponents()) {
154158
if (auto *Arg = Comp.getIndexExpr())
155159
CallArgs.insert(Arg);
@@ -322,6 +326,25 @@ static void diagSyntacticUseRestrictions(const Expr *E, const DeclContext *DC,
322326
return { true, E };
323327
}
324328

329+
/// Visit each component of the keypath and emit a diganostic if they
330+
/// refer to a member that has effects.
331+
void checkForEffectfulKeyPath(KeyPathExpr *keyPath) {
332+
for (const auto &component : keyPath->getComponents()) {
333+
if (component.hasDeclRef()) {
334+
auto decl = component.getDeclRef().getDecl();
335+
if (auto asd = dyn_cast<AbstractStorageDecl>(decl)) {
336+
if (auto getter = asd->getEffectfulGetAccessor()) {
337+
Ctx.Diags.diagnose(component.getLoc(),
338+
diag::effectful_keypath_component,
339+
asd->getDescriptiveKind());
340+
Ctx.Diags.diagnose(asd->getLoc(), diag::kind_declared_here,
341+
asd->getDescriptiveKind());
342+
}
343+
}
344+
}
345+
}
346+
}
347+
325348
void checkCheckedCastExpr(CheckedCastExpr *cast) {
326349
if (!isa<ConditionalCheckedCastExpr>(cast) && !isa<IsExpr>(cast))
327350
return;

lib/Sema/TypeCheckConcurrency.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,14 @@ ActorIsolationRestriction ActorIsolationRestriction::forDeclaration(
650650
isAccessibleAcrossActors = true;
651651
}
652652

653+
// Similarly, a computed property or subscript that has an 'async' getter
654+
// provides an asynchronous context, and has no restrictions.
655+
if (auto storageDecl = dyn_cast<AbstractStorageDecl>(decl)) {
656+
if (auto effectfulGetter = storageDecl->getEffectfulGetAccessor())
657+
if (effectfulGetter->hasAsync())
658+
isAccessibleAcrossActors = true;
659+
}
660+
653661
// Determine the actor isolation of the given declaration.
654662
switch (auto isolation = getActorIsolation(cast<ValueDecl>(decl))) {
655663
case ActorIsolation::ActorInstance:

lib/Sema/TypeCheckConstraints.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ bool TypeChecker::typeCheckForEachBinding(DeclContext *dc, ForEachStmt *stmt) {
603603
if (conformanceRef.hasEffect(EffectKind::Throws) &&
604604
stmt->getTryLoc().isInvalid()) {
605605
auto &diags = dc->getASTContext().Diags;
606-
diags.diagnose(stmt->getAwaitLoc(), diag::throwing_call_unhandled)
606+
diags.diagnose(stmt->getAwaitLoc(), diag::throwing_call_unhandled, "call")
607607
.fixItInsert(stmt->getAwaitLoc(), "try");
608608

609609
return failed();

lib/Sema/TypeCheckDeclObjC.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,14 @@ bool swift::isRepresentableInObjC(const VarDecl *VD, ObjCReason Reason) {
997997
if (checkObjCActorIsolation(VD, Reason))
998998
return false;
999999

1000+
// effectful computed properties cannot be represented, and its an error
1001+
// to mark them as @objc
1002+
if (VD->getEffectfulGetAccessor()) {
1003+
VD->diagnose(diag::effectful_not_representable_objc,
1004+
VD->getDescriptiveKind());
1005+
return false;
1006+
}
1007+
10001008
if (!Result) {
10011009
SourceRange TypeRange = VD->getTypeSourceRangeForDiagnostics();
10021010
// TypeRange can be invalid; e.g. '@objc let foo = SwiftType()'
@@ -1028,6 +1036,14 @@ bool swift::isRepresentableInObjC(const SubscriptDecl *SD, ObjCReason Reason) {
10281036
if (checkObjCActorIsolation(SD, Reason))
10291037
return false;
10301038

1039+
// effectful subscripts cannot be represented, and its an error
1040+
// to mark them as @objc
1041+
if (SD->getEffectfulGetAccessor()) {
1042+
SD->diagnose(diag::effectful_not_representable_objc,
1043+
SD->getDescriptiveKind());
1044+
return false;
1045+
}
1046+
10311047
// ObjC doesn't support class subscripts.
10321048
if (!SD->isInstanceMember()) {
10331049
SD->diagnose(diag::objc_invalid_on_static_subscript,

lib/Sema/TypeCheckDeclOverride.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,6 +1818,18 @@ static bool checkSingleOverride(ValueDecl *override, ValueDecl *base) {
18181818
return true;
18191819
}
18201820
}
1821+
1822+
// Make sure an effectful storage decl is only overridden by a storage
1823+
// decl with the same or fewer effect kinds.
1824+
if (!overrideASD->isLessEffectfulThan(baseASD, EffectKind::Async)) {
1825+
diags.diagnose(overrideASD, diag::override_with_more_effects,
1826+
overrideASD->getDescriptiveKind(), "async");
1827+
return true;
1828+
} else if (!overrideASD->isLessEffectfulThan(baseASD, EffectKind::Throws)) {
1829+
diags.diagnose(overrideASD, diag::override_with_more_effects,
1830+
overrideASD->getDescriptiveKind(), "throws");
1831+
return true;
1832+
}
18211833
}
18221834

18231835
// Various properties are only checked for the storage declarations

0 commit comments

Comments
 (0)