Definitely - and this is way more robust than what most people write even when they're trying to explicitly check for bogus results, but it's also significantly more hassle (versus ignoring the problem). I'm already a bit bothered by all the explicit integer casting I have to do in Swift, without having to prophylactically size everything up before arithmetic, and then down again afterwards.
Still, it's not a bad idea, and good of you to remind us. I should definitely keep that more in mind going forward.
When a function call is inlined, the compiler can inspect the specific arguments to the function, and if relevant, specialize the function call to those specific arguments; if ƒ can be inlined, then at every callsite, the compiler has the opportunity to reorder the arithmetic instructions (if it so chooses to) to avoid overflow.
However, when a function isn't inlined, the arguments are passed to its most general form — and there, there is no "all at once" for an expression like a - b + c: if the compiler doesn't know what the values of a, b, and c are, it can't know whether a - b will overflow, or whether a + c will overflow; it has to keep the order of operations specified in the expression.
And in that case, overflow is impossible to avoid. Unless an expression like a - b + c compiled to the equivalent of (a < b) ? (a + c - b) : (a - b + c), which has performance implications. If instead of a - b + c you have a - b + c * d - e - f - g, this gets pretty hairy.
This is subjective, but I'd take "I wrote this code but it never works" over "I wrote this code and it sometimes works and sometimes doesn't but I have no idea why" any day, because the former lets me build a consistent mental model of the language, and learn from it.
This could be 4 times faster than adding those bytes sequentially (be it as bytes or as widened Ints).
In fact you could add up to seven bytes at once allocating just 1 bit for the corresponding overflow values, although preparing that bit layout might take too long to be practical.
One thing I've been thinking is that it'd be nice to have a property-wrapper-like macro, which could let you declare a type as being outwardly of type Int or Int64 while being stored as something smaller, so that the opportunity for small integer types to infect client code is lessened. Something like:
struct Color {
@SmallInteger(UInt8)
var r, g, b, a: Int
}
might expand to something like:
var _r: UInt8
var r: Int {
get { return Int(_r) }
set { _r = UInt8(newValue) }
}
Top-level codegen is deeply weird for various reasons, and you should pretty much never look at it if you're trying to understand how Swift works more generally.
I was taking what I thought you were saying on faith, but I misunderstood what you were saying.
Can you clarify, then, what you were referring to with the literals vs variables point? Is it that the compiler chooses to mimic the imperative behaviour of the unfolded version, even though it technically doesn't have to in cases like this where the net result is actually valid?
Because of the way type-inference works with literals, integer literal expressions taken out of their surrounding context may be inferred to have different types then they do in context, which may change the meaning of the arithmetic expression. This is unfortunate because it can introduce subtle bugs when people refactor code. You can avoid this by always explicitly-typing expressions, but, well, almost no one does that.
OTOH, if it crashes sooner it might be better for developers to notice the potential issue sooner and change the code. And if it works - the potential bug could stay longer under the cover unnoticed and crash once in a while.
It's not the literal order of operations that matters here, it's when you validate the result. The compiler doesn't have to know the actual integer values - if it does, that simply allows it to optimise away the validation iff it can determine that the validation passes at compile time.
If I implied I meant for this to work only with literals, I apologise. From the start I've been assuming the associativity would apply to a whole expression (of variables and/or literals).
It could be great if it applied to a larger scope, if possible - e.g. a whole function - though I haven't thought that through.
What do you mean "sometimes"? I don't see any way this code's behaviour would change, without changing the code or the optimisation settings. Am I missing something?
I suppose changing compiler versions could break things, which is not ideal, though hopefully unlikely (it'd suggest a regression in performance). Not unprecedented, though. People already (should) test things thoroughly when they change compiler versions.
Ah, it sounds like we might have been talking about different things. It sounds like you're proposing a model where the compiler elides all overflow checking until the very end of an expression? (i.e., the AIR model proposed above) My comments have been based on the current model, where the stdlib operators validates overflow prior to every operation (and in which case, order of operations matters entirely).
I understood your prior comments to imply that the compiler should reorder the operations in such a way that overflow is guaranteed not to happen, so that the current model keeps working — so there is a failure mode in certain circumstances when the compiler isn't allowed to reorder the operations (e.g., inside of a non-inlined function).
Maybe this is an uncompelling example because it's so trite. But one can hopefully imagine that this could be a very real situation in serious code, e.g. where you'd been just implicitly using Ints for a bunch of things and then you decide to change them to another type, e.g. because it's more semantically correct that your type be unsigned, or you realise that the values are all quite small so you can save space with a narrower integer, etc.
Granted having to rearrange the order of arithmetic operations isn't usually a big deal, but it might be contrary to intuition or convention, in whatever problem domain or context you're working in. Like how we all write polynomials like y = x^2 + c, not y = c + x^2. Arbitrary, yet the reading difficulty is higher for the latter form.
To be clear, having to rearranging the order of arithmetic on literals isn't an important motivator for any change here. It's the cases involving variables, where it might be impossible to know at compile time what order of operations will succeed, that are of more concern.
Ah, yep. Within the constraints you were assuming, we are in agreement - I don't see any practical, consistent way for the compiler to actually reorder the operations in order to avoid under- or over-flow. Introduce a single variable, for example, instead of working purely in literals, and it's potentially impossible.
I mean, I wouldn't be opposed to it reordering things when it can, opportunistically, since that's still better (from a "my code no longer crashes, and works as intended" perspective). It just seems like a very minor improvement, possibly not worth the implementation cost.
I'm not necessarily advocating for the AIR model in its entirety - it sounds awesome, but I don't know what it would cost to implement, and it sounds like it's maybe too much to retrofit into Swift at this point per @scanon's comment. I really was just starting and focusing on the most basic case, of integer addition and subtraction within a single statement. To me that seems like it covers 90% of cases where this matters - lots of things involving calculating indices, for example, or bit shift magnitudes.
There are of course natural questions about if, how, and whether it should go further - e.g. why should my code crash because I changed:
let result = a - b + c
…to:
let soICanSeeItInTheDebugger = a - b
let result = soICanSeeItInTheDebugger + c
…so there's some key questions around that. But curiously nobody has yet probed that aspect of this.
All of Int's API is open-source (you wouldn't be able to build a functional Swift standard library from source if it wasn't). It's all defined in stdlib/public/core/Integers.swift and IntegerTypes.swift.gyb.
We probed this when we argued for it a decade ago, and this was certainly a major consideration. The rough idea at the time was to make it possible to talk about the intermediate type for AIR evaluation in the type system so you can do something like this
let intermediateValue: Integer = a - b
let result = intermediateValue + c
which isn't perfect, but gets you pretty far. I would say it would be better still if debuggers just worked with arbitrary subexpressions, but here we are.