Skip to content

[Sema] Improve diagnostics for non-escaping function types #15383

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -2805,6 +2805,9 @@ ERROR(assigning_noescape_to_escaping,none,
ERROR(general_noescape_to_escaping,none,
"using non-escaping parameter %0 in a context expecting an @escaping closure",
(Identifier))
ERROR(converting_noescape_to_type,none,
"converting non-escaping value to %0 may allow it to escape",
(Type))

ERROR(capture_across_type_decl,none,
"%0 declaration cannot close over value %1 defined in outer scope",
Expand Down
28 changes: 27 additions & 1 deletion lib/Sema/CSApply.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7817,7 +7817,9 @@ bool ConstraintSystem::applySolutionFix(Expr *expr,
// If we didn't manage to resolve directly to an expression, we don't
// have a great diagnostic to give, so bail.
if (!resolved || !resolved->getAnchor() ||
!resolved->getPath().empty())
(!resolved->getPath().empty() &&
fix.first.getKind() != FixKind::ExplicitlyEscaping &&
fix.first.getKind() != FixKind::ExplicitlyEscapingToAny))
return false;

Expr *affected = resolved->getAnchor();
Expand Down Expand Up @@ -7974,6 +7976,30 @@ bool ConstraintSystem::applySolutionFix(Expr *expr,
}
return false;
}

case FixKind::ExplicitlyEscaping: {
auto path = locator->getPath();
auto *anchor = locator->getAnchor();

if (path.empty())
return false;

auto &last = path.back();
if (last.getKind() == ConstraintLocator::Archetype) {
auto *archetype = last.getArchetype();
TC.diagnose(anchor->getLoc(), diag::converting_noescape_to_type,
archetype);
return true;
}
break;
}

case FixKind::ExplicitlyEscapingToAny: {
auto *anchor = locator->getAnchor();
TC.diagnose(anchor->getLoc(), diag::converting_noescape_to_type,
getASTContext().TheAnyType);
return true;
}
}

// FIXME: It would be really nice to emit a follow-up note showing where
Expand Down
2 changes: 1 addition & 1 deletion lib/Sema/CSBindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ ConstraintSystem::getPotentialBindingForRelationalConstraint(
// should be allowed to escape. As a result we allow anything
// passed in to escape.
if (auto *fnTy = type->getAs<AnyFunctionType>())
if (typeVar->getImpl().getArchetype())
if (typeVar->getImpl().getArchetype() && !shouldAttemptFixes())
type = fnTy->withExtInfo(fnTy->getExtInfo().withNoEscape(false));

// Check whether we can perform this binding.
Expand Down
60 changes: 51 additions & 9 deletions lib/Sema/CSSimplify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -672,9 +672,16 @@ matchCallArguments(ConstraintSystem &cs, ConstraintKind kind,

// Disallow assignment of noescape function to parameter of type
// Any. Allowing this would allow these functions to escape.
if (auto *fnTy = argType->getAs<AnyFunctionType>())
if (fnTy->isNoEscape())
if (auto *fnTy = argType->getAs<AnyFunctionType>()) {
if (fnTy->isNoEscape()) {
// Allow assigned of 'no-escape' function with recorded fix.
if (cs.shouldAttemptFixes() &&
!cs.recordFix(FixKind::ExplicitlyEscaping, locator))
return cs.getTypeMatchSuccess();

return cs.getTypeMatchFailure(locator);
}
}

return cs.getTypeMatchSuccess();
}
Expand Down Expand Up @@ -1317,6 +1324,10 @@ ConstraintSystem::matchExistentialTypes(Type type1, Type type2,
if (!fnTy || !fnTy->isNoEscape())
return getTypeMatchSuccess();

if (shouldAttemptFixes() &&
!recordFix(FixKind::ExplicitlyEscapingToAny, locator))
return getTypeMatchSuccess();

return getTypeMatchFailure(locator);
}

Expand Down Expand Up @@ -1490,10 +1501,18 @@ ConstraintSystem::matchTypesBindTypeVar(
// Disallow bindings of noescape functions to type variables that
// represent an opened archetype. If we allowed this it would allow
// the noescape function to potentially escape.
if (auto *fnTy = type->getAs<AnyFunctionType>())
if (fnTy->isNoEscape())
if (typeVar->getImpl().getArchetype())
if (auto *fnTy = type->getAs<AnyFunctionType>()) {
if (fnTy->isNoEscape() && typeVar->getImpl().getArchetype()) {
if (shouldAttemptFixes()) {
if (recordFix(FixKind::ExplicitlyEscaping, locator))
return getTypeMatchFailure(locator);

// Allow no-escape function to be bound with recorded fix.
} else {
return getTypeMatchFailure(locator);
}
}
}

// Check whether the type variable must be bound to a materializable
// type.
Expand Down Expand Up @@ -2011,9 +2030,20 @@ ConstraintSystem::matchTypes(Type type1, Type type2, ConstraintKind kind,
// Penalize conversions to Any, and disallow conversions of
// noescape functions to Any.
if (kind >= ConstraintKind::Conversion && type2->isAny()) {
if (auto *fnTy = type1->getAs<AnyFunctionType>())
if (fnTy->isNoEscape())
return getTypeMatchFailure(locator);
if (auto *fnTy = type1->getAs<AnyFunctionType>()) {
if (fnTy->isNoEscape()) {
if (shouldAttemptFixes()) {
if (recordFix(FixKind::ExplicitlyEscapingToAny, locator))
return getTypeMatchFailure(locator);

// Allow 'no-escape' functions to be converted to 'Any'
// with a recorded fix that helps us to properly diagnose
// such situations.
} else {
return getTypeMatchFailure(locator);
}
}
}

increaseScore(ScoreKind::SK_EmptyExistentialConversion);
}
Expand Down Expand Up @@ -4757,7 +4787,17 @@ bool ConstraintSystem::recordFix(Fix fix, ConstraintLocatorBuilder locator) {
if (worseThanBestSolution())
return true;

Fixes.push_back({fix, getConstraintLocator(locator)});
auto *loc = getConstraintLocator(locator);
auto existingFix =
llvm::find_if(Fixes, [&](std::pair<Fix, ConstraintLocator *> &e) {
// If we already have a fix like this recorded, let's not do it again,
// this situation might happen when the same fix kind is applicable to
// different overload choices.
return e.first == fix && e.second == loc;
});

if (existingFix == Fixes.end())
Fixes.push_back({fix, loc});

return false;
}
Expand Down Expand Up @@ -4802,6 +4842,8 @@ ConstraintSystem::simplifyFixConstraint(Fix fix, Type type1, Type type2,
return result;
}

case FixKind::ExplicitlyEscaping:
case FixKind::ExplicitlyEscapingToAny:
case FixKind::CoerceToCheckedCast:
llvm_unreachable("handled elsewhere");
}
Expand Down
3 changes: 3 additions & 0 deletions lib/Sema/Constraint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,9 @@ StringRef Fix::getName(FixKind kind) {
return "fix: add address-of";
case FixKind::CoerceToCheckedCast:
return "fix: as to as!";
case FixKind::ExplicitlyEscaping:
case FixKind::ExplicitlyEscapingToAny:
return "fix: add @escaping";
}

llvm_unreachable("Unhandled FixKind in switch.");
Expand Down
11 changes: 9 additions & 2 deletions lib/Sema/Constraint.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ enum RememberChoice_t : bool {
enum class FixKind : uint8_t {
/// Introduce a '!' to force an optional unwrap.
ForceOptional,

/// Introduce a '?.' to begin optional chaining.
OptionalChaining,

Expand All @@ -241,9 +241,14 @@ enum class FixKind : uint8_t {

/// Introduce a '&' to take the address of an lvalue.
AddressOf,

/// Replace a coercion ('as') with a forced checked cast ('as!').
CoerceToCheckedCast,

/// Mark function type as explicitly '@escaping'.
ExplicitlyEscaping,
/// Mark function type as explicitly '@escaping' to be convertable to 'Any'.
ExplicitlyEscapingToAny,
};

/// Describes a fix that can be applied to a constraint before visiting it.
Expand Down Expand Up @@ -279,6 +284,8 @@ class Fix {
LLVM_ATTRIBUTE_DEPRECATED(void dump(ConstraintSystem *cs) const
LLVM_ATTRIBUTE_USED,
"only for use within the debugger");

bool operator==(Fix const &b) { return Kind == b.Kind && Data == b.Data; }
};


Expand Down
16 changes: 6 additions & 10 deletions test/Constraints/function.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ sr590((1, 2))

// SR-2657: Poor diagnostics when function arguments should be '@escaping'.
private class SR2657BlockClass<T> {
// expected-note@-1 {{'T' declared as parameter to type 'SR2657BlockClass'}}
// expected-note@-2 {{'T' declared as parameter to type 'SR2657BlockClass'}}
let f: T
init(f: T) { self.f = f }
}
Expand All @@ -96,17 +94,15 @@ func takesAny(_: Any) {}

func foo(block: () -> (), other: () -> Int) { // expected-note 2 {{parameter 'block' is implicitly non-escaping}}
let _ = SR2657BlockClass(f: block)
// expected-error@-1 {{generic parameter 'T' could not be inferred}}
// expected-note@-2 {{explicitly specify the generic arguments to fix this issue}}
// expected-error@-1 {{converting non-escaping value to 'T' may allow it to escape}}
let _ = SR2657BlockClass<()->()>(f: block)
// expected-error@-1 {{passing non-escaping parameter 'block' to function expecting an @escaping closure}}
let _: SR2657BlockClass<()->()> = SR2657BlockClass(f: block)
// expected-error@-1 {{generic parameter 'T' could not be inferred}}
// expected-note@-2 {{explicitly specify the generic arguments to fix this issue}}
// expected-error@-1 {{converting non-escaping value to 'T' may allow it to escape}}
let _: SR2657BlockClass<()->()> = SR2657BlockClass<()->()>(f: block)
// expected-error@-1 {{passing non-escaping parameter 'block' to function expecting an @escaping closure}}
_ = SR2657BlockClass<Any>(f: block) // expected-error{{function produces expected type '()'; did you mean to call it with '()'?}}
_ = SR2657BlockClass<Any>(f: other) // expected-error{{function produces expected type 'Int'; did you mean to call it with '()'?}}
takesAny(block) // expected-error{{function produces expected type '()'; did you mean to call it with '()'?}}
takesAny(other) // expected-error{{function produces expected type 'Int'; did you mean to call it with '()'?}}
_ = SR2657BlockClass<Any>(f: block) // expected-error {{converting non-escaping value to 'Any' may allow it to escape}}
_ = SR2657BlockClass<Any>(f: other) // expected-error {{converting non-escaping value to 'Any' may allow it to escape}}
takesAny(block) // expected-error {{converting non-escaping value to 'Any' may allow it to escape}}
takesAny(other) // expected-error {{converting non-escaping value to 'Any' may allow it to escape}}
}
4 changes: 2 additions & 2 deletions test/Constraints/function_conversion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func takesAny(_ f: Any) {}

func twoFns(_ f: (Int) -> Int, _ g: @escaping (Int) -> Int) {
// expected-note@-1 {{parameter 'f' is implicitly non-escaping}}
takesAny(f) // expected-error {{argument type '(Int) -> Int' does not conform to expected type 'Any'}}
takesAny(f) // expected-error {{converting non-escaping value to 'Any' may allow it to escape}}
takesAny(g)
var h = g
h = f // expected-error {{assigning non-escaping parameter 'f' to an @escaping closure}}
Expand All @@ -63,4 +63,4 @@ var escapingParam: (@escaping (Int) -> Int) -> () = consumeEscaping
noEscapeParam = escapingParam // expected-error {{cannot assign value of type '(@escaping (Int) -> Int) -> ()' to type '((Int) -> Int) -> ()}}

escapingParam = takesAny
noEscapeParam = takesAny // expected-error {{cannot assign value of type '(Any) -> ()' to type '((Int) -> Int) -> ()'}}
noEscapeParam = takesAny // expected-error {{converting non-escaping value to 'Any' may allow it to escape}}
2 changes: 1 addition & 1 deletion test/Constraints/suspicious_bit_casts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

func escapeByBitCast(f: () -> ()) -> () -> () {
return unsafeBitCast(f, to: (() -> ()).self)
// expected-error@-1 {{generic parameter 'T' could not be inferred}}
// expected-error@-1 {{converting non-escaping value to 'T' may allow it to escape}}
}

func changeFnRep(f: @escaping () -> ()) -> @convention(block) () -> () {
Expand Down
8 changes: 3 additions & 5 deletions test/attr/attr_noescape.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ func takesGenericClosure<T>(_ a : Int, _ fn : @noescape () -> T) {} // expected-
var globalAny: Any = 0

func assignToGlobal<T>(_ t: T) {
// expected-note@-1 {{in call to function 'assignToGlobal'}}
globalAny = t
}

Expand Down Expand Up @@ -56,15 +55,14 @@ func takesNoEscapeClosure(_ fn : () -> Int) {
takesGenericClosure(4, fn) // ok
takesGenericClosure(4) { fn() } // ok.

_ = [fn] // expected-error {{type of expression is ambiguous without more context}}
_ = [fn] // expected-error {{converting non-escaping value to 'Element' may allow it to escape}}
_ = [doesEscape(fn)] // expected-error {{'(() -> Int) -> ()' is not convertible to '(@escaping () -> Int) -> ()'}}
_ = [1 : fn] // expected-error {{type of expression is ambiguous without more context}}
_ = [1 : fn] // expected-error {{converting non-escaping value to 'Value' may allow it to escape}}
_ = [1 : doesEscape(fn)] // expected-error {{passing non-escaping parameter 'fn' to function expecting an @escaping closure}}
_ = "\(doesEscape(fn))" // expected-error {{passing non-escaping parameter 'fn' to function expecting an @escaping closure}}
_ = "\(takesArray([fn]))" // expected-error {{using non-escaping parameter 'fn' in a context expecting an @escaping closure}}

// FIXME: Generate a more specific error about the non-escaping parameter 'fn'
assignToGlobal(fn) // expected-error {{generic parameter 'T' could not be inferred}}
assignToGlobal(fn) // expected-error {{converting non-escaping value to 'T' may allow it to escape}}
}

class SomeClass {
Expand Down