Skip to content
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

[SR-14440] Confusing diagnostics when attempting to use a trailing closure in an if let #56796

Open
swift-ci opened this issue Apr 1, 2021 · 5 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself diagnostics QoI Bug: Diagnostics Quality of Implementation parser Area → compiler: The legacy C++ parser

Comments

@swift-ci
Copy link
Collaborator

swift-ci commented Apr 1, 2021

Previous ID SR-14440
Radar rdar://problem/76116607
Original Reporter lithium3141 (JIRA User)
Type Bug
Environment

Xcode 12.4 (12D4e) on macOS Big Sur 11.2.3 (20D91)

Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug, DiagnosticsQoI, Parser
Assignee None
Priority Medium

md5: e4e6e9982f83a4f89bfc8bb0ef3cf88b

Issue Description:

I tried to write a snippet that mapped over a ClosedRange, looking for the first Int that — when formatted into a particular String — met a condition. (The exact format and condition aren't especially relevant.) My first pass was to wrap the entire chain in an `if let`, as such:

if let string = (1 ... 9999)
.map { String(format: "%@_%04d", "foo", $0) }
.first(where: { $0.hasSuffix("_0003") })
{
print(string)
}

This does not compile, producing multiple errors:

  • "error: anonymous closure argument not contained in a closure" on the $0 in the map

  • "error: generic parameter 'T' could not be inferred" on the ... operator

  • "error: generic parameter 'T' could not be inferred" on "string" in the print()

If I compile this snippet with a plain `let` instead of an `if let`, it compiles and produces a `String?` value, as expected.

I think the original `if let` snippet should also be able to compile, binding `string` inside the body of the `if`.

@typesanitizer
Copy link

@swift-ci create

@xAlien95
Copy link
Contributor

xAlien95 commented Apr 1, 2021

Trailing closure syntax and statements with blocks such as if or guard..else may collide. For this reason there's a specific warning+fixit in Swift when that happens:

Trailing closure in this context is confusable with the body of the statement; pass as a parenthesized argument to silence this warning

Replace ' { ... }' with '({ ... })'

You can wrap the map closure in round brackets (or wrap in round brackets the whole chain, i.e. from (1...9999) up to the beginning of the if body) to have your code snippet working as expected in the meantime.


A minimal reproducer for this bug may be:

struct S {
  let foo = true
  func bar(_: () -> ()) -> Self { self }
}

if S().bar {}
.foo { print(3) }

If you press enter after .bar {}, you don't get the next line indented and there's no code completion:

if S().bar{}
.<HERE>  // no suggestions

// it works inline
if S().bar {}.<HERE>  // suggests .foo

// it works if wrapped in brackets
if (S().bar {}
    .<HERE>)  // suggests .foo

@beccadax
Copy link
Contributor

lithium3141 (JIRA User) This is expected behavior. Trailing closure syntax is not supported in the condition of an if, while, for, switch, etc. statement because a bare { while parsing the condition is interpreted as the start of the statement's body:

if let string = (1 ... 9999)
    .map { /* this is treated as the body of the 'if' statement! */ } 

Of course, the map(_:) method actually requires a closure argument, but the Swift parser doesn't know that—it decides that this block is a statement body, not a trailing closure, at a stage where the compiler doesn't and can't know what the name map will eventually resolve to. You can make this code parse by parenthesizing the closure:

if let string = (1 ... 9999)
    .map({ /* this is unambiguously a closure argument */ })

On the other hand, the diagnostic could certainly be clearer. I'm going to edit this report so it tracks the poor diagnostic instead of the surprising-but-correct behavior.

@beccadax
Copy link
Contributor

(Specifically, if we see $0 in a statement body, or a bare in keyword while parsing the first statement in that body, we should suspect that the block was supposed to be a trailing closure for a call in the condition and offer to parenthesize it.)

@xAlien95
Copy link
Contributor

@beccadax, I think the problem resides in the fact that the parser acts differently if the statement spans multiple lines. Given the minimal reproducer I posted above

struct S {
  let foo = true
  func bar(_: () -> ()) -> Self { self }
}

if we have the following in a single line

if S().bar {}.foo { print(3) }

then the correct warning is emitted:

Trailing closure in this context is confusable with the body of the statement;
pass as a parenthesized argument to silence this warning

[fix] Replace ' {}' with '({})'

If, however, we split it in two lines

if S().bar {}
.foo { print(3) }

then it gets parsed as two statements, i.e. as

if S().bar {}; .foo { print(3) }

leading to the surprising errors.


If lithium3141 (JIRA User)'s statement were on a single line

if let string = (1 ... 9999).map { String(format: "%@_%04d", "foo", $0) }.first { $0.hasSuffix("_0003") } { print(string) }

it would be parsed and compiled correctly.

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself diagnostics QoI Bug: Diagnostics Quality of Implementation parser Area → compiler: The legacy C++ parser
Projects
None yet
Development

No branches or pull requests

4 participants