Skip to content

Commit aab6469

Browse files
committed
ConstraintSystem: clarify consuming conversions
There are a number of implicit conversions in Swift, such as to Optional and to an existential, which are now possible for noncopyable types. But all type casts are consuming operations for noncopyable types. So it's confusing when a function that takes a borrowed argument of optional type appears to be consuming: ``` func f(_ x: borrowing NC?) { ... } let x = NC() f(x) f(x) // error! ``` So, rather than for people to write `x as T?` around all implicit conversions, require them to write `consume x` around expressions that will consume some lvalue. Since that makes it much more clear what the consequences will be. Expressions like `f(g())`, where you're passing an rvalue to the callee, are not confusing. And those are exactly the expressions you're not allowed to write `consume` for, anyway. fixes rdar://127450418 (cherry picked from commit e55e247)
1 parent 5002612 commit aab6469

File tree

5 files changed

+259
-34
lines changed

5 files changed

+259
-34
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7774,6 +7774,10 @@ ERROR(noncopyable_objc_enum, none,
77747774
"noncopyable enums cannot be marked '@objc'", ())
77757775
ERROR(moveOnly_not_allowed_here,none,
77767776
"'@_moveOnly' attribute is only valid on structs or enums", ())
7777+
WARNING(consume_expression_needed_for_cast,none,
7778+
"implicit conversion to %0 is consuming", (Type))
7779+
NOTE(add_consume_to_silence_warning,none,
7780+
"add 'consume' to silence this warning", ())
77777781
ERROR(consume_expression_not_passed_lvalue,none,
77787782
"'consume' can only be applied to a local binding ('let', 'var', or parameter)", ())
77797783
ERROR(consume_expression_partial_copyable,none,

include/swift/Sema/ConstraintSystem.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6574,6 +6574,13 @@ TypeVariableType *TypeVariableType::getNew(const ASTContext &C, unsigned ID,
65746574
/// underlying forced downcast expression.
65756575
ForcedCheckedCastExpr *findForcedDowncast(ASTContext &ctx, Expr *expr);
65766576

6577+
/// Assuming the expression appears in a consuming context,
6578+
/// if it does not already have an explicit `consume`,
6579+
/// can I add `consume` around this expression?
6580+
///
6581+
/// \param module represents the module in which the expr appears
6582+
bool canAddExplicitConsume(ModuleDecl *module, Expr *expr);
6583+
65776584
// Count the number of overload sets present
65786585
// in the expression and all of the children.
65796586
class OverloadSetCounter : public ASTWalker {

lib/Sema/CSApply.cpp

Lines changed: 106 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,45 @@ static bool buildObjCKeyPathString(KeyPathExpr *E,
355355
return true;
356356
}
357357

358+
/// Since a cast to an optional will consume a noncopyable type, check to see
359+
/// if injecting the value into an optional here will potentially be confusing.
360+
static bool willHaveConfusingConsumption(Type type,
361+
ConstraintLocatorBuilder locator,
362+
ConstraintSystem &cs) {
363+
assert(type);
364+
if (!type->isNoncopyable())
365+
return false; /// If it's a copyable type, there's no confusion.
366+
367+
auto loc = cs.getConstraintLocator(locator);
368+
if (!loc)
369+
return true;
370+
371+
auto path = loc->getPath();
372+
if (path.empty())
373+
return true;
374+
375+
switch (loc->getPath().back().getKind()) {
376+
case ConstraintLocator::FunctionResult:
377+
case ConstraintLocator::ClosureResult:
378+
case ConstraintLocator::ClosureBody:
379+
case ConstraintLocator::ContextualType:
380+
case ConstraintLocator::CoercionOperand:
381+
return false; // These last-uses won't be confused for borrowing.
382+
383+
case ConstraintLocator::ApplyArgToParam: {
384+
auto argLoc = loc->castLastElementTo<LocatorPathElt::ApplyArgToParam>();
385+
auto paramFlags = argLoc.getParameterFlags();
386+
if (paramFlags.getOwnershipSpecifier() == ParamSpecifier::Consuming)
387+
return false; // Parameter already declares 'consuming'.
388+
389+
return true;
390+
}
391+
392+
default:
393+
return true;
394+
}
395+
}
396+
358397
namespace {
359398

360399
/// Rewrites an expression by applying the solution of a constraint
@@ -3200,10 +3239,12 @@ namespace {
32003239
}
32013240

32023241
private:
3203-
/// A list of "suspicious" optional injections that come from
3204-
/// forced downcasts.
3242+
/// A list of "suspicious" optional injections.
32053243
SmallVector<InjectIntoOptionalExpr *, 4> SuspiciousOptionalInjections;
32063244

3245+
/// A list of implicit coercions of noncopyable types.
3246+
SmallVector<Expr *, 4> ConsumingCoercions;
3247+
32073248
/// Create a member reference to the given constructor.
32083249
Expr *applyCtorRefExpr(Expr *expr, Expr *base, SourceLoc dotLoc,
32093250
DeclNameLoc nameLoc, bool implicit,
@@ -4425,9 +4466,9 @@ namespace {
44254466
if (choice == 0) {
44264467
// Convert the subexpression.
44274468
Expr *sub = expr->getSubExpr();
4428-
4429-
sub = solution.coerceToType(sub, expr->getCastType(),
4430-
cs.getConstraintLocator(sub));
4469+
auto subLoc =
4470+
cs.getConstraintLocator(sub, ConstraintLocator::CoercionOperand);
4471+
sub = solution.coerceToType(sub, expr->getCastType(), subLoc);
44314472
if (!sub)
44324473
return nullptr;
44334474

@@ -5475,41 +5516,69 @@ namespace {
54755516
assert(OpenedExistentials.empty());
54765517

54775518
auto &ctx = cs.getASTContext();
5519+
auto *module = dc->getParentModule();
54785520

54795521
// Look at all of the suspicious optional injections
54805522
for (auto injection : SuspiciousOptionalInjections) {
5481-
auto *cast = findForcedDowncast(ctx, injection->getSubExpr());
5482-
if (!cast)
5483-
continue;
5484-
5485-
if (isa<ParenExpr>(injection->getSubExpr()))
5486-
continue;
5523+
if (auto *cast = findForcedDowncast(ctx, injection->getSubExpr())) {
5524+
if (!isa<ParenExpr>(injection->getSubExpr())) {
5525+
ctx.Diags.diagnose(
5526+
injection->getLoc(), diag::inject_forced_downcast,
5527+
cs.getType(injection->getSubExpr())->getRValueType());
5528+
auto exclaimLoc = cast->getExclaimLoc();
5529+
ctx.Diags
5530+
.diagnose(exclaimLoc, diag::forced_to_conditional_downcast,
5531+
cs.getType(injection)->getOptionalObjectType())
5532+
.fixItReplace(exclaimLoc, "?");
5533+
ctx.Diags
5534+
.diagnose(cast->getStartLoc(),
5535+
diag::silence_inject_forced_downcast)
5536+
.fixItInsert(cast->getStartLoc(), "(")
5537+
.fixItInsertAfter(cast->getEndLoc(), ")");
5538+
}
5539+
}
5540+
}
54875541

5488-
ctx.Diags.diagnose(
5489-
injection->getLoc(), diag::inject_forced_downcast,
5490-
cs.getType(injection->getSubExpr())->getRValueType());
5491-
auto exclaimLoc = cast->getExclaimLoc();
5542+
// Diagnose the implicit coercions of noncopyable values that happen in
5543+
// a context where it isn't "obviously" consuming already.
5544+
for (auto *coercion : ConsumingCoercions) {
5545+
assert(coercion->isImplicit());
54925546
ctx.Diags
5493-
.diagnose(exclaimLoc, diag::forced_to_conditional_downcast,
5494-
cs.getType(injection)->getOptionalObjectType())
5495-
.fixItReplace(exclaimLoc, "?");
5547+
.diagnose(coercion->getLoc(),
5548+
diag::consume_expression_needed_for_cast,
5549+
cs.getType(coercion));
54965550
ctx.Diags
5497-
.diagnose(cast->getStartLoc(), diag::silence_inject_forced_downcast)
5498-
.fixItInsert(cast->getStartLoc(), "(")
5499-
.fixItInsertAfter(cast->getEndLoc(), ")");
5551+
.diagnose(coercion->getLoc(),
5552+
diag::add_consume_to_silence_warning)
5553+
.fixItInsert(coercion->getStartLoc(), "consume ");
55005554
}
55015555
}
55025556

55035557
/// Diagnose an optional injection that is probably not what the
5504-
/// user wanted, because it comes from a forced downcast.
5505-
void diagnoseOptionalInjection(InjectIntoOptionalExpr *injection) {
5558+
/// user wanted, because it comes from a forced downcast, or from an
5559+
/// implicitly consumed noncopyable type.
5560+
void diagnoseOptionalInjection(InjectIntoOptionalExpr *injection,
5561+
ConstraintLocatorBuilder locator) {
55065562
// Check whether we have a forced downcast.
5507-
auto *cast =
5508-
findForcedDowncast(cs.getASTContext(), injection->getSubExpr());
5509-
if (!cast)
5510-
return;
5511-
5512-
SuspiciousOptionalInjections.push_back(injection);
5563+
if (findForcedDowncast(cs.getASTContext(), injection->getSubExpr()))
5564+
SuspiciousOptionalInjections.push_back(injection);
5565+
5566+
/// Check if it needs an explicit consume, due to this being a cast.
5567+
auto *module = dc->getParentModule();
5568+
auto origType = cs.getType(injection->getSubExpr());
5569+
if (willHaveConfusingConsumption(origType, locator, cs) &&
5570+
canAddExplicitConsume(module, injection->getSubExpr()))
5571+
ConsumingCoercions.push_back(injection);
5572+
}
5573+
5574+
void diagnoseExistentialErasureOf(Expr *fromExpr, Expr *toExpr,
5575+
ConstraintLocatorBuilder locator) {
5576+
auto *module = dc->getParentModule();
5577+
auto fromType = cs.getType(fromExpr);
5578+
if (willHaveConfusingConsumption(fromType, locator, cs) &&
5579+
canAddExplicitConsume(module, fromExpr)) {
5580+
ConsumingCoercions.push_back(toExpr);
5581+
}
55135582
}
55145583
};
55155584
} // end anonymous namespace
@@ -5780,7 +5849,7 @@ Expr *ExprRewriter::coerceOptionalToOptional(Expr *expr, Type toType,
57805849
while (diff--) {
57815850
Type type = toOptionals[diff];
57825851
expr = cs.cacheType(new (ctx) InjectIntoOptionalExpr(expr, type));
5783-
diagnoseOptionalInjection(cast<InjectIntoOptionalExpr>(expr));
5852+
diagnoseOptionalInjection(cast<InjectIntoOptionalExpr>(expr), locator);
57845853
}
57855854

57865855
return expr;
@@ -6909,8 +6978,11 @@ Expr *ExprRewriter::coerceToType(Expr *expr, Type toType,
69096978
return coerceSuperclass(expr, toType);
69106979

69116980
case ConversionRestrictionKind::Existential:
6912-
case ConversionRestrictionKind::MetatypeToExistentialMetatype:
6913-
return coerceExistential(expr, toType, locator);
6981+
case ConversionRestrictionKind::MetatypeToExistentialMetatype: {
6982+
auto coerced = coerceExistential(expr, toType, locator);
6983+
diagnoseExistentialErasureOf(expr, coerced, locator);
6984+
return coerced;
6985+
}
69146986

69156987
case ConversionRestrictionKind::ClassMetatypeToAnyObject: {
69166988
assert(ctx.LangOpts.EnableObjCInterop &&
@@ -6939,7 +7011,7 @@ Expr *ExprRewriter::coerceToType(Expr *expr, Type toType,
69397011

69407012
auto *result =
69417013
cs.cacheType(new (ctx) InjectIntoOptionalExpr(expr, toType));
6942-
diagnoseOptionalInjection(result);
7014+
diagnoseOptionalInjection(result, locator);
69437015
return result;
69447016
}
69457017

@@ -7633,7 +7705,7 @@ Expr *ExprRewriter::coerceToType(Expr *expr, Type toType,
76337705
if (!expr) return nullptr;
76347706

76357707
auto *result = cs.cacheType(new (ctx) InjectIntoOptionalExpr(expr, toType));
7636-
diagnoseOptionalInjection(result);
7708+
diagnoseOptionalInjection(result, locator);
76377709
return result;
76387710
}
76397711

lib/Sema/TypeCheckConstraints.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,6 +2317,10 @@ static Expr *lookThroughBridgeFromObjCCall(ASTContext &ctx, Expr *expr) {
23172317
/// underlying forced downcast expression.
23182318
ForcedCheckedCastExpr *swift::findForcedDowncast(ASTContext &ctx, Expr *expr) {
23192319
expr = expr->getSemanticsProvidingExpr();
2320+
2321+
// Look through a consume, just in case.
2322+
if (auto consume = dyn_cast<ConsumeExpr>(expr))
2323+
expr = consume->getSubExpr();
23202324

23212325
// Simple case: forced checked cast.
23222326
if (auto forced = dyn_cast<ForcedCheckedCastExpr>(expr)) {
@@ -2368,6 +2372,18 @@ ForcedCheckedCastExpr *swift::findForcedDowncast(ASTContext &ctx, Expr *expr) {
23682372
return nullptr;
23692373
}
23702374

2375+
bool swift::canAddExplicitConsume(ModuleDecl *module, Expr *expr) {
2376+
expr = expr->getSemanticsProvidingExpr();
2377+
2378+
// Is it already wrapped in a `consume`?
2379+
if (isa<ConsumeExpr>(expr))
2380+
return false;
2381+
2382+
// Is this expression valid to wrap inside a `consume`?
2383+
auto diags = findSyntacticErrorForConsume(module, SourceLoc(), expr);
2384+
return diags.empty();
2385+
}
2386+
23712387
void ConstraintSystem::forEachExpr(
23722388
Expr *expr, llvm::function_ref<Expr *(Expr *)> callback) {
23732389
struct ChildWalker : ASTWalker {

test/Sema/moveonly_casts.swift

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// RUN: %target-typecheck-verify-swift
2+
3+
struct NC: ~Copyable {}
4+
5+
func testBorrowing(_ v: borrowing NC?) {}
6+
func testConsuming(_ v: consuming NC?) {}
7+
func testInout(_ v: inout NC?) {}
8+
9+
class MethodSir {
10+
func borrow(_ v: borrowing NC?) {}
11+
func consume(_ v: consuming NC?) {}
12+
}
13+
14+
func testExplicitCasts() {
15+
let nc = NC()
16+
_ = nc as NC?
17+
}
18+
19+
func testCalls() {
20+
let method = MethodSir()
21+
let foo = NC()
22+
testBorrowing(foo) // expected-warning {{implicit conversion to 'NC?' is consuming}}
23+
// expected-note@-1 {{add 'consume' to silence this warning}} {{17-17=consume }}
24+
testBorrowing(consume foo)
25+
testBorrowing(foo as NC?)
26+
27+
method.borrow(foo) // expected-warning {{implicit conversion to 'NC?' is consuming}}
28+
// expected-note@-1 {{add 'consume' to silence this warning}} {{17-17=consume }}
29+
method.borrow(consume foo)
30+
31+
testConsuming(foo)
32+
testConsuming(consume foo)
33+
34+
var optNC: NC? = NC() // ConstraintLocator::ContextualType
35+
testInout(&optNC)
36+
}
37+
38+
func testReturn() -> NC? {
39+
let foo = NC()
40+
return foo // ConstraintLocator::ContextualType
41+
}
42+
43+
func higherOrder(_ f: () -> NC?) -> NC? {
44+
if let nc = f() {
45+
nc // ConstraintLocator::ContextualType
46+
} else {
47+
nil
48+
}
49+
}
50+
func callHigherOrder() {
51+
let nc = NC()
52+
let _ = higherOrder { nc } // ConstraintLocator::ClosureBody
53+
54+
let _ = higherOrder { return nc } // ConstraintLocator::ClosureBody
55+
}
56+
57+
58+
func delay(_ f: @autoclosure () -> NC?) -> NC? { f() }
59+
60+
func testDelay() {
61+
let nc = NC()
62+
let _ = delay(nc) // expected-warning {{implicit conversion to 'NC?' is consuming}}
63+
// expected-note@-1 {{add 'consume' to silence this warning}} {{17-17=consume }}
64+
}
65+
66+
struct PropCity {
67+
var harmless1: NC? { NC() }
68+
var harmless2: NC? {
69+
get { return NC() }
70+
}
71+
72+
subscript(_ i: Int) -> NC? { return NC() }
73+
74+
func chk(_ t: borrowing NC!) {}
75+
func chkWithDefaultArg(_ oath: borrowing NC? = NC()) {}
76+
func test(_ nc: consuming NC) {
77+
chk(nc) // expected-warning {{implicit conversion to 'NC?' is consuming}}
78+
// expected-note@-1 {{add 'consume' to silence this warning}} {{9-9=consume }}
79+
80+
chk(consume nc)
81+
82+
chkWithDefaultArg()
83+
chkWithDefaultArg(nc) // expected-warning {{implicit conversion to 'NC?' is consuming}}
84+
// expected-note@-1 {{add 'consume' to silence this warning}} {{23-23=consume }}
85+
}
86+
}
87+
88+
protocol Veggie: ~Copyable {}
89+
struct Carrot: ~Copyable, Veggie {}
90+
91+
func restockBorrow(_ x: borrowing any Veggie & ~Copyable) {}
92+
func restockConsume(_ x: consuming any Veggie & ~Copyable) {}
93+
94+
func checkExistential() {
95+
let carrot = Carrot()
96+
restockBorrow(carrot) // expected-warning {{implicit conversion to 'any Veggie & ~Copyable' is consuming}}
97+
// expected-note@-1 {{add 'consume' to silence this warning}} {{17-17=consume }}
98+
restockBorrow(consume carrot)
99+
100+
restockConsume(carrot)
101+
}
102+
103+
func genericErasure<T: Veggie & ~Copyable>(_ veg: consuming T) {
104+
restockBorrow(veg) // expected-warning {{implicit conversion to 'any Veggie & ~Copyable' is consuming}}
105+
// expected-note@-1 {{add 'consume' to silence this warning}} {{17-17=consume }}
106+
restockBorrow(consume veg)
107+
restockBorrow(veg as any Veggie & ~Copyable)
108+
restockConsume(veg)
109+
110+
let _ = veg as any Veggie & ~Copyable
111+
}
112+
113+
extension Veggie where Self: ~Copyable {
114+
func inspect(_ b: borrowing any Veggie & ~Copyable) {}
115+
}
116+
extension Carrot {
117+
consuming func check() {
118+
inspect(self) // expected-warning {{implicit conversion to 'any Veggie & ~Copyable' is consuming}}
119+
// expected-note@-1 {{add 'consume' to silence this warning}} {{13-13=consume }}
120+
inspect(consume self)
121+
inspect(self as any Veggie & ~Copyable)
122+
123+
let _: any Veggie & ~Copyable = self
124+
}
125+
}
126+

0 commit comments

Comments
 (0)