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-7478] type checker pretty confused #50021

Open
weissi opened this issue Apr 19, 2018 · 13 comments
Open

[SR-7478] type checker pretty confused #50021

weissi opened this issue Apr 19, 2018 · 13 comments
Labels
bug A deviation from expected or documented behavior. Also: expected but undesirable behavior. compiler The Swift compiler in itself type checker Area → compiler: Semantic analysis

Comments

@weissi
Copy link
Member

weissi commented Apr 19, 2018

Previous ID SR-7478
Radar rdar://problem/39560933
Original Reporter @weissi
Type Bug
Status Reopened
Resolution
Additional Detail from JIRA
Votes 0
Component/s Compiler
Labels Bug, TypeChecker
Assignee None
Priority Medium

md5: 28e7ee8b48b9b181087d7b3524a3c9c9

Issue Description:

The type checker in quite a few versions of Swift is very confused and thinks Box<Int> is equal to Void. See this little demo program:

struct Box<T> {
    private let value: T
    init(_ value: T) {
        self.value = value
    }
    func extract() -> T {
        return self.value
    }
}

func boxIt<T>(_ fn: () -> T) -> Box<T> {
    return Box(fn())
}

let y: Box<Int> = boxIt { return boxIt { 3 } }.extract()
let x: Void     = boxIt { return boxIt { 3 } }.extract()

it clearly should compile error on the last line telling that Box<Int> isn't Void. This is what various Swift versions do. (My jw-swift-... wrappers just invoke xcrun with the right parameters)

Swift 4.0 release:

$ jw-swift-4.0 swiftc --version
Apple Swift version 4.0.3 (swift-4.0.3-RELEASE)
Target: x86_64-apple-macosx10.9
$ jw-swift-4.0 swiftc test.swift 
test.swift:16:34: warning: result of call to 'boxIt' is unused
let x: Void     = boxIt { return boxIt { 3 } }.extract()
                                 ^     ~~~~~

which is VERY BAD as that compiled just fine with just a warning!

Swift 4.1 release (the only one that caught this error)

$ jw-swift-4.1 swiftc --version
Apple Swift version 4.1 (swift-4.1-RELEASE)
Target: x86_64-apple-darwin17.6.0
$ jw-swift-4.1 swiftc test.swift 
test.swift:16:48: error: cannot convert value of type 'Box<Int>' to specified type 'Void'
let x: Void     = boxIt { return boxIt { 3 } }.extract()
                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~

which is good as it errors.

very recent Swift master

$ jw-swift-latest swiftc --version
Apple Swift version 4.2-dev (LLVM d14a2b25f2, Clang c38020c511, Swift da10607e45)
Target: x86_64-apple-darwin17.6.0
johannes:/tmp
$ jw-swift-latest swiftc test.swift 
test.swift:16:34: warning: result of call to 'boxIt' is unused
let x: Void     = boxIt { return boxIt { 3 } }.extract()
                                 ^     ~~~~~

or

$ /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2018-04-16-a.xctoolchain/usr/bin/swiftc test.swift 
test.swift:16:34: warning: result of call to 'boxIt' is unused
let x: Void     = boxIt { return boxIt { 3 } }.extract()
                                 ^     ~~~~~

which is very bad as it compiled JUST FINE again, so regressed back to where 4.0.3 was 🙁

which is bad as as regressed back to where Swift 4.0 was 🙁

@weissi
Copy link
Member Author

weissi commented Apr 19, 2018

@swift-ci create

@belkadan
Copy link
Contributor

I think this is correct-ish behavior, in that we consider it legal to convert a closure-returning-T to a closure-returning-Void. @xedin?

@weissi
Copy link
Member Author

weissi commented Apr 20, 2018

@belkadan are you sure? The inner

boxIt { 3 }

returns a Box<Int>, we can't just throw that away, make it Void and then return an EventLoopFuture<Void> from

boxIt { boxIt { 3 } }

@belkadan
Copy link
Contributor

Sure you can. The closure

{ result boxIt { 3 } }

has type () -> Box<Int>, then that gets converted to () -> Void by throwing away the result, and now you've made a Box<Void>.

@xedin
Copy link
Member

xedin commented Apr 21, 2018

@weissi @belkadan is right, there is an implicit conversion from `() -> T` to `() -> Void` for trailing closures which we support and because the type-checker is very smart it infers contextual `Void` as `T` (as only possible for this expression) and then converts result of `() -> Box<Int>` into `() -> Void`.

@weissi
Copy link
Member Author

weissi commented Apr 23, 2018

@xedin & @belkadan why does it do this implicit conversion? If I really wanted an implicit conversion I could write

boxIt { _ = boxIt { 3 } }

@xedin
Copy link
Member

xedin commented Apr 23, 2018

This is related to the behavior of the type-checker where it would try to find a solution based on the information given and if there is a possible implicit conversion, it would be taken but the score is going to get increased, so if there are solutions without such conversion they would be considered a "better" match. For single-expression closures we don't model `return` statement so both { return 42 } and { 42 } are equivalent which leads to this behavior.

@weissi
Copy link
Member Author

weissi commented Apr 24, 2018

thanks @xedin that makes sense. Isn't that a bug though? Where it caused trouble for us is the following in the NIO tests: (https://github.com/apple/swift-nio/blob/9f01374169e4f19785edfebdeda0bfef9ae44abf/Tests/NIOTests/SocketChannelTest.swift#L428-L437)

XCTAssertNoThrow(try channel.eventLoop.submit {
            channel.register().map { () -> Void in
                channel.connect(to: serverChannel.localAddress!, promise: connectPromise)
            }.map { () -> Void in
                XCTAssertFalse(connectPromise.futureResult.isFulfilled)
                // The close needs to happen in the then { ... } block to ensure we close the channel
                // before we have the chance to register it for .write.
                channel.close(promise: closePromise)
            }
        }.wait().wait() as Void)

the return value from channel.eventLoop.submit is EventLoopFuture<EventLoopFuture<Void>> which is odd quite unusual (outside of tests) but it makes sense: The outer future fulfils when inner operation channel.register()... has been started, the inner one completes when the channel has registered (we told epoll/kqueue about it) and is connected (TCP connection up). So for the tests it's crucial to write .wait().wait() here otherwise you'll have a race. And to communicate to the reviewers that I did the right thing I started adding as Void. Oddly enough with Swift != 4.1 I can remove one of the .wait() calls, still write as Void and sadly it will still compile. I found that highly surprising and unhelpful.

What is reason we allow implicitly converting a closure () -> T where T != Void to () -> Void ? Again, if the user actually wanted that, there's syntax for that: {{ _ = fn() }} no?

@xedin
Copy link
Member

xedin commented Apr 24, 2018

What is reason we allow implicitly converting a closure () -> T where T != Void to () -> Void ?

I don't think I follow this argument, in the minimized example as in this one the contextual type is Void. If there is no Void type in the context it would not be inferred e.g. `let _ = { 42 }` and `let _ = { return 42 }` would both return `() -> Int`.

@weissi
Copy link
Member Author

weissi commented Apr 24, 2018

@xedin but you could write `{ _ = 42 }` (if 42 had side-effects) to make it `() -> Void` if you really wanted. If I write `{ 42 }` I really mean `() -> Int` and I don't want it to be converted to `() -> Void`

@xedin
Copy link
Member

xedin commented Apr 24, 2018

Sure, but once again if your context requires () -> Void there is an implicit conversion for that, e.g.

func foo(_: () -> Void) {}
foo { return 42 }

This is very much intentional way the language works but only for `Void` and only single-expression closures. If this is good or bad aside, changing that would require evolution proposal.

@weissi
Copy link
Member Author

weissi commented Apr 24, 2018

@xedin wow, I did not know this would compile, I think that's really bad but you're right evolution is the place to sort that out.

@weissi
Copy link
Member Author

weissi commented Apr 24, 2018

@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 type checker Area → compiler: Semantic analysis
Projects
None yet
Development

No branches or pull requests

3 participants