Associative arithmetic operation order matters in Swift?

Aha. It calls Builtin.${u}${x.llvmName}_Int${bits}. We are one step closer to the truth :slight_smile:
Where the heck that would be? Ultimately I'd like to see the overflow logic itself.

That's only for division; the other operations actually call Builtin.${u}${x.llvmName}_with_overflow_Int${bits}, which expands to something like Builtin.uadd_with_overflow_Int32, which is just the Swift binding¹ of an LLVM instruction, which you can lookup in the language reference. None of this stuff is magic.

A bare llvm.uadd.with.overflow.* would get lowered by the compiler backend to something like an ADD followed by a SETcc on most platforms (using the x86 opcode names because they're generally more familiar to people, but ARM codegen is pretty much isomorphic), but in practice you're usually either consuming the overflow to implement multiword arithmetic or trapping if it's set, so you end up with something more like ADD + ADC or ADD + Jcc.

¹ There's a tiny footnote here that Builtin.uadd_with_overflow_Int32 is actually defined in the Swift compiler (in files that include include/swift/AST/Builtins.def) rather than literally being the bare LLVM intrinsic for diagnostic purposes² (Builtins.def L148):

/// These builtins are analogous the similarly named llvm intrinsics. The
/// difference between the two is that these are not expected to overflow,
/// so we should produce a compile time error if we can statically prove
/// that they do.

The implementations have some diagnostic hooks and then turn around and lower to the LLVM intrinsic.

² This is arguably subtly wrong and presupposes that these are used only to implement the trapping operations--really the trapping operations should probably have their own implementations that call these wrappers and the overflow operations should call the llvm intrinsics directly.

7 Likes

Interesting idea, but I think it should be deeper in the compiler than macros, almost invisible from the outside. Unless you / the particular component wants to drill into it.


Once upon a time we had 1-complement computers where to negate a number you just invert all bits (without adding one), meaning there were two bit patterns for zero (or perhaps those CPU did something special to auto convert FFFF to 0). Those computers were rare but existed nevertheless, and they existed to the point that even C/C++ languages supported those architectures by specifying that short int range was "at least" between -0x7FFF to 0x7FFF (so it could be either that or the normal 2-complement range from -0x8000 to 0x7FFF). And because we had INT_MIN, etc constants - we could have written algorithms compatible with those architectures without too much hassle (although in fairness the algorithms would probably still break if you didn't test them on those architectures). For many algorithms the particular range was (and still is) not important, so even if Int16 ranges between -0x7000 to 0x7000 – many normal apps would be happy. In principle we could sacrifice one value of each integer type to mean NaN / overflow. Example:

// assuming non AIR semantics below, once overflow - always overflow
Int8.nan           // -0x80
UInt8.nan          // 0xFF
Int16.nan          // -0x8000
Int16.nan          // 0xFFFF
....
Int64.nan          // -0x8000000000000000
UInt64.nan         // 0xFFFFFFFFFFFFFFFF

var x: UInt8 = 200 // ✅ 200
x += 100           // 🚸 0xFF Nan / overflow
x -= 100           // 🚸 0xFF Nan / overflow
let index = Int(x) // 🚸 -0x80000000 Nan / overflow
array[index]       // ❌ trap. here it is super important to know correct value 
print(index)       // ❌ trap. ditto here it is super important to know correct value 

Those algorithms that do require all bit patterns of integers can't use these, and they could use full range integers. It would be somewhat similar to advanced features like &+ and other overflowing operations – the presumption is not many apps need that.

I appreciate this is a huge compatibility issue. For example you have a struct:

struct S: Codable {
    var x: Int16
}

And it's codable and you want to recreate it from existing JSON's and there could be values like -32768 in those, and it was not a problem 50 years ago when 1-complement CPU's existed because we didn't have JSON back then. In this case you'd need to change Int16 to Int32, similar to how Java handled unsigned numbers before they were invented by Java creators - without UInt32 to represent 0x80000000 you'd just switch to the next big signed type - Int64 in this case.

Alternatively the struct could hold some RawInt16 type - the one that supports full range, and depending upon the task in question you either use that, or convert between it and the other Int16 type – the one that's one value shorter – during that conversion it makes sense to trap on the NaN value.

Could we store that bit outside of the value itself? Yes, but besides compatibility issue now it will also become spec issue, as all Ints are now 9 bytes instead of 8, and even 16 if they need to be aligned – not good.

Is it possible to carry that overflow bit without actually storing it in the value? I don't know. Maybe it's possible but I don't see how:

var s = S()
s.x = 0x7FFF
s.x += 1 // where do we store this overflow bit?
// so far so "good"

var s2 = s
s2.x // x  here must be invalid somehow, or the above `"s2 = s"` line must trap

This approach doesn't solve the OP issue, but it is somewhat similar to the AIR approach in regards to not trapping for overflow after each and every operation, but doing this only in some key places when having the valid value is critical. It also allows the opt-in checks like `number.isValid where you can do what you want, instead of what be currently have (trapping, unless you use & operations or cumbersome "addingReportingOverflow" functions).