Skip to content

[SR-13755] Convenience operators on BinaryInteger causing subtle bugs due to unintuitive type inference #56152

@regexident

Description

@regexident
Previous ID SR-13755
Radar rdar://problem/70485231
Original Reporter @regexident
Type Bug
Additional Detail from JIRA
Votes 2
Component/s
Labels Bug
Assignee None
Priority Medium

md5: 75125d4cd126a1caa0e7e05edfa98e00

relates to:

  • SR-4984 Integer literal not inferred as unsigned in != comparison

Issue Description:

The existence of this method on BinaryInteger

static func == <Other>(lhs: Self, rhs: Other) -> Bool where Other : BinaryInteger

… has caused me quite some headache today, and wasted several hours spent debugging.

The fact that this method (and its brothers & sisters <=, >=, etc.) performs the following conversion on the rhs

let rhsAsSelf = Self(truncatingIfNeeded: rhs)

… makes the following code behave in rather unexpected ways:

func nonGeneric(_ t: UInt8) {
    precondition(t != 0b0)
    precondition(t == ~0b0)
}

func generic<T: BinaryInteger>(_ t: T) {
    precondition(t != 0b0)
    precondition(t == ~0b0)
}

let t: UInt8 = ~0b0

nonGeneric(t)
generic(t)

One would expect both calls to succeed (and most importantly: behave identical!).
Alas the second one has its second precondition fail:

Terminated due to signal: ILLEGAL INSTRUCTION (4)
Precondition failed: file Untitled.swift, line 8

Why? Let's investigate.

First we'll add the following helper function to help in inspecting rhs's type:

func spyType<T>(_ t: T) -> T {
    print("-", type(of: t))
    return t
}

Then we wrap rhs with spyType(…):

func nonGeneric(_ t: UInt8) {
    print(#function)
    precondition(t != spyType(0b0))
    precondition(t == spyType(~0b0))
}

func generic<T: BinaryInteger>(_ t: T) {
    print(#function)
    precondition(t != spyType(0b0))
    precondition(t == spyType(~0b0))
}

… to find this printed on the console:

nonGeneric(_:)
- Int
- UInt8
generic(_:)
- Int
- Int

The type-checker seems to infer rhs to be T: BinaryInteger, and then picks the default: Int, which then overflows on Self(truncatingIfNeeded: rhs).

The reason we get the correct UInt8 in there at all is that Int does not have an ~ operator, forcing the type-checker to infer the correct type of UInt8, which does.

While this is correct behavior from the perspective of the type-checker (thanks to the existence of above operator on BinaryInteger), it leads to subtle and utterly unexpected bugs and thus should be considered a bug, I think.

This is bad. So bad I wonder if above (convenience!) operator should exist at all, as it's the cause for all of this. There are just too many ways this could lead to unexpected behavior.

At the very least the compiler should emit a warning à la "The type of rhs is ambiguous, use an explicit type".

But even then naïvely wrapping the literal in a `UInt8(…)` to make it explicit doesn't solve the issue either and actually causes a fatal error now (but hey, at least it crashes now!):

func nonGeneric(_ t: UInt8) {
    print(#function)
    precondition(t != UInt8(spyType(0b0)))
    precondition(t == UInt8(spyType(~0b0)))
}

func generic<T: BinaryInteger>(_ t: T) {
    print(#function)
    precondition(t != T(spyType(0b0)))
    precondition(t == T(spyType(~0b0)))
}
nonGeneric(_:)
- Int
- Int
Fatal error: Negative value is not representable: file Swift/Integers.swift, line 3439

One needs to actually promote the rvalues to explicitly typed lvalues to make the above code behave as expected:

func nonGeneric(_ t: UInt8) {
    print(#function)
    let emptyMask: UInt8 = 0b0
    precondition(t != spyType(emptyMask))
    let fullMask: UInt8 = ~0b0
    precondition(t == spyType(fullMask))
}

func generic<T: BinaryInteger>(_ t: T) {
    print(#function)
    let emptyMask: T = 0b0
    precondition(t != spyType(emptyMask))
    let fullMask: T = ~0b0
    precondition(t == spyType(fullMask))
}

Which now FINALLY produces the expected behavior …

nonGeneric(_:)
- UInt8
- UInt8
generic(_:)
- UInt8
- UInt8

As a developer this is not an acceptable workaround to me, tbh.

Any user who does not already have a solid understanding, of how type-inference in Swift works, would be utterly lost here. And even those who do will have a hard time finding the culprit.

It should not behave this way and it should definitely should not require me to bend over backwards to make it work at all.

(ALSO, I CAN HAZ TYPE ASCRIPTION? KTHXBAI!)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA deviation from expected or documented behavior. Also: expected but undesirable behavior.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions