Skip to content

Commit e55e247

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
1 parent 1f83017 commit e55e247

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
@@ -7848,6 +7848,10 @@ ERROR(noncopyable_objc_enum, none,
78487848
"noncopyable enums cannot be marked '@objc'", ())
78497849
ERROR(moveOnly_not_allowed_here,none,
78507850
"'@_moveOnly' attribute is only valid on structs or enums", ())
7851+
WARNING(consume_expression_needed_for_cast,none,
7852+
"implicit conversion to %0 is consuming", (Type))
7853+
NOTE(add_consume_to_silence_warning,none,
7854+
"add 'consume' to silence this warning", ())
78517855
ERROR(consume_expression_not_passed_lvalue,none,
78527856
"'consume' can only be applied to a local binding ('let', 'var', or parameter)", ())
78537857
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
@@ -6582,6 +6582,13 @@ TypeVariableType *TypeVariableType::getNew(const ASTContext &C, unsigned ID,
65826582
/// underlying forced downcast expression.
65836583
ForcedCheckedCastExpr *findForcedDowncast(ASTContext &ctx, Expr *expr);
65846584

6585+
/// Assuming the expression appears in a consuming context,
6586+
/// if it does not already have an explicit `consume`,
6587+
/// can I add `consume` around this expression?
6588+
///
6589+
/// \param module represents the module in which the expr appears
6590+
bool canAddExplicitConsume(ModuleDecl *module, Expr *expr);
6591+
65856592
// Count the number of overload sets present
65866593
// in the expression and all of the children.
65876594
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
@@ -3241,10 +3280,12 @@ namespace {
32413280
}
32423281

32433282
private:
3244-
/// A list of "suspicious" optional injections that come from
3245-
/// forced downcasts.
3283+
/// A list of "suspicious" optional injections.
32463284
SmallVector<InjectIntoOptionalExpr *, 4> SuspiciousOptionalInjections;
32473285

3286+
/// A list of implicit coercions of noncopyable types.
3287+
SmallVector<Expr *, 4> ConsumingCoercions;
3288+
32483289
/// Create a member reference to the given constructor.
32493290
Expr *applyCtorRefExpr(Expr *expr, Expr *base, SourceLoc dotLoc,
32503291
DeclNameLoc nameLoc, bool implicit,
@@ -4466,9 +4507,9 @@ namespace {
44664507
if (choice == 0) {
44674508
// Convert the subexpression.
44684509
Expr *sub = expr->getSubExpr();
4469-
4470-
sub = solution.coerceToType(sub, expr->getCastType(),
4471-
cs.getConstraintLocator(sub));
4510+
auto subLoc =
4511+
cs.getConstraintLocator(sub, ConstraintLocator::CoercionOperand);
4512+
sub = solution.coerceToType(sub, expr->getCastType(), subLoc);
44724513
if (!sub)
44734514
return nullptr;
44744515

@@ -5516,41 +5557,69 @@ namespace {
55165557
assert(OpenedExistentials.empty());
55175558

55185559
auto &ctx = cs.getASTContext();
5560+
auto *module = dc->getParentModule();
55195561

55205562
// Look at all of the suspicious optional injections
55215563
for (auto injection : SuspiciousOptionalInjections) {
5522-
auto *cast = findForcedDowncast(ctx, injection->getSubExpr());
5523-
if (!cast)
5524-
continue;
5525-
5526-
if (isa<ParenExpr>(injection->getSubExpr()))
5527-
continue;
5564+
if (auto *cast = findForcedDowncast(ctx, injection->getSubExpr())) {
5565+
if (!isa<ParenExpr>(injection->getSubExpr())) {
5566+
ctx.Diags.diagnose(
5567+
injection->getLoc(), diag::inject_forced_downcast,
5568+
cs.getType(injection->getSubExpr())->getRValueType());
5569+
auto exclaimLoc = cast->getExclaimLoc();
5570+
ctx.Diags
5571+
.diagnose(exclaimLoc, diag::forced_to_conditional_downcast,
5572+
cs.getType(injection)->getOptionalObjectType())
5573+
.fixItReplace(exclaimLoc, "?");
5574+
ctx.Diags
5575+
.diagnose(cast->getStartLoc(),
5576+
diag::silence_inject_forced_downcast)
5577+
.fixItInsert(cast->getStartLoc(), "(")
5578+
.fixItInsertAfter(cast->getEndLoc(), ")");
5579+
}
5580+
}
5581+
}
55285582

5529-
ctx.Diags.diagnose(
5530-
injection->getLoc(), diag::inject_forced_downcast,
5531-
cs.getType(injection->getSubExpr())->getRValueType());
5532-
auto exclaimLoc = cast->getExclaimLoc();
5583+
// Diagnose the implicit coercions of noncopyable values that happen in
5584+
// a context where it isn't "obviously" consuming already.
5585+
for (auto *coercion : ConsumingCoercions) {
5586+
assert(coercion->isImplicit());
55335587
ctx.Diags
5534-
.diagnose(exclaimLoc, diag::forced_to_conditional_downcast,
5535-
cs.getType(injection)->getOptionalObjectType())
5536-
.fixItReplace(exclaimLoc, "?");
5588+
.diagnose(coercion->getLoc(),
5589+
diag::consume_expression_needed_for_cast,
5590+
cs.getType(coercion));
55375591
ctx.Diags
5538-
.diagnose(cast->getStartLoc(), diag::silence_inject_forced_downcast)
5539-
.fixItInsert(cast->getStartLoc(), "(")
5540-
.fixItInsertAfter(cast->getEndLoc(), ")");
5592+
.diagnose(coercion->getLoc(),
5593+
diag::add_consume_to_silence_warning)
5594+
.fixItInsert(coercion->getStartLoc(), "consume ");
55415595
}
55425596
}
55435597

55445598
/// Diagnose an optional injection that is probably not what the
5545-
/// user wanted, because it comes from a forced downcast.
5546-
void diagnoseOptionalInjection(InjectIntoOptionalExpr *injection) {
5599+
/// user wanted, because it comes from a forced downcast, or from an
5600+
/// implicitly consumed noncopyable type.
5601+
void diagnoseOptionalInjection(InjectIntoOptionalExpr *injection,
5602+
ConstraintLocatorBuilder locator) {
55475603
// Check whether we have a forced downcast.
5548-
auto *cast =
5549-
findForcedDowncast(cs.getASTContext(), injection->getSubExpr());
5550-
if (!cast)
5551-
return;
5552-
5553-
SuspiciousOptionalInjections.push_back(injection);
5604+
if (findForcedDowncast(cs.getASTContext(), injection->getSubExpr()))
5605+
SuspiciousOptionalInjections.push_back(injection);
5606+
5607+
/// Check if it needs an explicit consume, due to this being a cast.
5608+
auto *module = dc->getParentModule();
5609+
auto origType = cs.getType(injection->getSubExpr());
5610+
if (willHaveConfusingConsumption(origType, locator, cs) &&
5611+
canAddExplicitConsume(module, injection->getSubExpr()))
5612+
ConsumingCoercions.push_back(injection);
5613+
}
5614+
5615+
void diagnoseExistentialErasureOf(Expr *fromExpr, Expr *toExpr,
5616+
ConstraintLocatorBuilder locator) {
5617+
auto *module = dc->getParentModule();
5618+
auto fromType = cs.getType(fromExpr);
5619+
if (willHaveConfusingConsumption(fromType, locator, cs) &&
5620+
canAddExplicitConsume(module, fromExpr)) {
5621+
ConsumingCoercions.push_back(toExpr);
5622+
}
55545623
}
55555624
};
55565625
} // end anonymous namespace
@@ -5821,7 +5890,7 @@ Expr *ExprRewriter::coerceOptionalToOptional(Expr *expr, Type toType,
58215890
while (diff--) {
58225891
Type type = toOptionals[diff];
58235892
expr = cs.cacheType(new (ctx) InjectIntoOptionalExpr(expr, type));
5824-
diagnoseOptionalInjection(cast<InjectIntoOptionalExpr>(expr));
5893+
diagnoseOptionalInjection(cast<InjectIntoOptionalExpr>(expr), locator);
58255894
}
58265895

58275896
return expr;
@@ -6950,8 +7019,11 @@ Expr *ExprRewriter::coerceToType(Expr *expr, Type toType,
69507019
return coerceSuperclass(expr, toType);
69517020

69527021
case ConversionRestrictionKind::Existential:
6953-
case ConversionRestrictionKind::MetatypeToExistentialMetatype:
6954-
return coerceExistential(expr, toType, locator);
7022+
case ConversionRestrictionKind::MetatypeToExistentialMetatype: {
7023+
auto coerced = coerceExistential(expr, toType, locator);
7024+
diagnoseExistentialErasureOf(expr, coerced, locator);
7025+
return coerced;
7026+
}
69557027

69567028
case ConversionRestrictionKind::ClassMetatypeToAnyObject: {
69577029
assert(ctx.LangOpts.EnableObjCInterop &&
@@ -6980,7 +7052,7 @@ Expr *ExprRewriter::coerceToType(Expr *expr, Type toType,
69807052

69817053
auto *result =
69827054
cs.cacheType(new (ctx) InjectIntoOptionalExpr(expr, toType));
6983-
diagnoseOptionalInjection(result);
7055+
diagnoseOptionalInjection(result, locator);
69847056
return result;
69857057
}
69867058

@@ -7674,7 +7746,7 @@ Expr *ExprRewriter::coerceToType(Expr *expr, Type toType,
76747746
if (!expr) return nullptr;
76757747

76767748
auto *result = cs.cacheType(new (ctx) InjectIntoOptionalExpr(expr, toType));
7677-
diagnoseOptionalInjection(result);
7749+
diagnoseOptionalInjection(result, locator);
76787750
return result;
76797751
}
76807752

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)