Skip to content

Re-implement "no-escape parameter call restriction" as SIL pass #23907

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

Conversation

slavapestov
Copy link
Contributor

@slavapestov slavapestov commented Apr 9, 2019

My previous PR #23878 exposed a correctness issue. The following code was formerly rejected as an escaping use of a noescape function:

func test(fn: (() -> ()) -> ()) {
   func foo() {
     fn {}
   }
   fn(foo)
}

However, there is no escaping violation here, and the new implementation of escaping capture diagnostics made the bogus error go away. On the other hand, this example does violate the "no-escape parameter call restriction" rule, but we failed to diagnose it because the implementation of that rule was not taking transitive captures of local functions into account.

Finally, the Sema implementation of the NPCR rule had a bug that could cause an infinite recursion.

The new implementation looks at SIL instead, which makes it a bit simpler and fixes these correctness issues.

Fixes rdar://problem/34496304.

This fixes a test involving transitive captures of local functions,
as well as an infinite recursion possible with the old code.

Fixes <rdar://problem/34496304>.
@slavapestov slavapestov requested review from rjmccall and atrick April 9, 2019 20:49
@slavapestov
Copy link
Contributor Author

@swift-ci Please test

@slavapestov
Copy link
Contributor Author

@swift-ci Please test source compatibility

@swift-ci
Copy link
Contributor

swift-ci commented Apr 9, 2019

Build failed
Swift Test Linux Platform
Git Sha - e2cb057

@slavapestov
Copy link
Contributor Author

@swift-ci Please test Linux

@slavapestov slavapestov merged commit d82b88c into swiftlang:master Apr 9, 2019
@atrick
Copy link
Contributor

atrick commented Apr 10, 2019

@slavapestov I'm a little worried that we don't have any verification to ensure that follows the uses of non-escaping function parameters to ensure that all possible SIL patterns are recognized, thereby proving that this diagnostic is complete. But this is still a big improvement!

static void checkForViolationsAtInstruction(ASTContext &Context,
DeclContext *DC,
SILInstruction *I) {
if (auto *PAI = dyn_cast<PartialApplyInst>(I))
checkPartialApply(Context, DC, PAI);

if (isa<ApplyInst>(I) || isa<TryApplyInst>(I)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use FullApplySite::isa here? The code above seems like it would work with coroutines, and there might be coroutine closures in the future.

@jtbandes
Copy link
Contributor

jtbandes commented Nov 13, 2020

@slavapestov Is there some intuition behind this new error? I see it's about overlapping variable modifications, but I can't quite wrap my head around what a pathological case would look like.

I came across this error in the real world while trying to de-duplicate some wrapper functions I made. A reduced example:

Given some library functions I don't control,

func foo(_ f: () -> Void) -> Error?
func bar(_ f: () -> Void) -> Error?

I wrapped them like so to make them more Swift-friendly:

struct DefaultError: Error {}

func betterFoo<T>(_ body: () throws -> T) throws -> T {  // (not rethrows, because foo() itself can return an error)
    var result = Result<T, Error>.failure(DefaultError())
    if let error = foo({ result = Result(catching: body) }) {
        throw error
    }
    return try result.get()
}

func betterBar<T>(_ body: () throws -> T) throws -> T {
    var result = Result<T, Error>.failure(DefaultError())
    if let error = bar({ result = Result(catching: body) }) {
        throw error
    }
    return try result.get()
}

I didn't like having two copies of the same code, so I tried to deduplicate them with a helper function...but now I get this error.

func betterFoo<T>(_ body: () throws -> T) throws -> T {
    return try betterize(foo, body)
}
func betterBar<T>(_ body: () throws -> T) throws -> T {
    return try betterize(bar, body)
}

func betterize<T>(_ original: (() -> Void) -> Error?, _ body: () throws -> T) throws -> T {
    var result = Result<T, Error>.failure(DefaultError())

    // error: passing a closure which captures a non-escaping function parameter 'body' to a call to a non-escaping function parameter can allow re-entrant modification of a variable
    if let error = original({ result = Result(catching: body) }) {
        throw error
    }
    return try result.get()
}

Is there a workaround (other than presumably sprinkling in some @escapings) or at least an example of what could go wrong, to motivate why this code isn't accepted?

Thanks! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants