`Bool?` is not optional?

What in the type inference is going on here? (thread.stack is an array)

warning: left side of nil coalescing operator '??' has non-optional type 'Bool?', so the right side is never used
        let goLeft = { thread.stack.last.map { $0 < 0 } ?? true }
                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^~~~~~~~

The variable goLeft is inferred as returning Bool?? of all things, and I have no idea where it got that from. Weirdly enough, this seems to depend on the types: changing 0 to 0 as Int32 or .zero fixes the error. Since it's relevant, the element type of thread.stack is a custom struct adopting FixedWidthInteger & SignedInteger. (I have no idea why Int32 works but an unannotated literal doesn't.) Adding an explicit type annotation -- let goLeft: () -> Bool = ... -- also fixes the issue, but feels unnecessary.

1 Like

What am I missing here?

func main() {
    let stack: [Int] = []
    let goLeft = { stack.last.map { $0 < 0 } ?? true }
    print(goLeft)
}

main()

The above compiles just fine with Xcode 15.0rc, with type of goLeft being inferred as () -> Bool.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Yeah, it doesn't happen with the built-in integer types, only with my custom struct. I just uploaded the code so you can take a look: https://github.com/bbrk24/trilangle-swift/blob/30c30b4222759e7d1429a70f852d5c67b1421206/Sources/ThreadManager.swift#L91

I’ve mocked the question like this and it produces the same warning

struct MyThread<T> where T: FixedWidthInteger & SignedInteger {
    var stack: [T] = []
}

func test() {
    let thread = MyThread<Int8>()
    let _ = { thread.stack.last.map { $0 < 0 } ?? true }   //warning
    let _ = { thread.stack.last.map { $0 < (0 as Int) } ?? true }  //ok
}

I don’t know why but if the generic type is Int instead of Int8 then the warning goes away

2 Likes

Yeah this is... weird. From a quick look at the -debug-constraints output it appears that we are finding two solutions—one where ?? is bound to the (@autoclosure () -> T?, T) -> T overload, and another where it ends up bound to the (@autoclosure () -> T?, T?) -> T? overload. The first solution is the one we want, and in the second solution we end up binding T to Bool?, so from the 'internal' viewpoint of the ?? function, the left-hand side is non-optional because it returns T rather than T?.

The reason this second solution ends up getting picked over the first one is that it picks for the < overload the implementation from BinaryInteger, typed as (Self, Other) -> Bool where Other : BinaryInteger. This means that the integer literal 0 is able to be bound to the default integer literal type Int. In the first solution, we pick Int8.< which is typed as (Int8, Int8) -> Bool, requiring the integer literal to be typed as Int8. This non-default literal is considered less desirable than a solution which requires some optional promotions, but that penalty doesn't apply when you explicitly type the literal via as.

It's a lot of constraint solving output, so I haven't dug into it further to figure out why we pick the overloads we do, or exactly why T gets bound to Bool? in the second solution, but hopefully this helps demystify things at least a little. cc @hborla @xedin it looks like this has been around at least since Xcode 14, is there a known bug tracking this to either of your knowledges?

9 Likes

Yeah, the score bit for "non-default literal" is ranked higher than optional promotion. It's been this way forever, but my personal opinion is this is never the rule that you want, and it makes generic code that uses literals extremely easy to get wrong. I'd love to explore changing this ranking rule in the future.

I definitely have a bug for this issue sitting around somewhere.

7 Likes