I think in this situation you’d use *% operators to say that you’re happy for wrapping overflow. Maybe widening operators wouldn’t be the standard *, +, - operators. Maybe those are left alone.
So I don’t think you want to promote the operands as such. Many CPUs have widening multiplies that take (for example) 16-bit inputs and give a 32-bit result. You don’t want to force it to do a 32-bit * 32-bit multiply to give a 64-bit result which you then truncate. It’s more that you just want a way to say this is a “widening” operator.
Correct, but this would be uN and iM (as the following discussion correctly concludes). The arithmetic compatibility rule doesn’t impact mixed-width arithmetic much, except where multiplication is concerned, and that’s a consequence of how it’s formulated: I’m not sure that it should have that effect. Briefly: unsigned multiplication reliably overflows when both operands are larger than the square root of the maximum value, so you actually can fulfill “higher percent of valid values are legal to operate” with a decently smaller signed participant. It’s fair to find this result surprising. Multiplication without widening is pretty dangerous.
Speaking of widening:
I definitely roll my eyes when I have to fix ‘promotion bugs’, but I don’t think this is the best description of the problem.
On some level, you asked for u8 arithmetic, and you’re given it: Zig doesn’t promote for you. The first thing I want to say about implicit promotion is that C has it, and that’s bad.
But Zig does have result locations, and it does seem, unfair is perhaps the word, that if I’ve explicitly written out a u16 result location (or know that I have one, same difference) I get an overflow bug instead of a promotion.
I’d really want to think through all of the consequences of a promotion rule involving operations and result locations before I could agree that we should do it. Arithmetic can get arbitrarily complex, does anything counterintuitive happen when the result location is distributed into larger formulas? Does signedness pose any gotchas?
It’s not that I disagree, it’s that I’m not prepared to agree. I’ve tried writing exactly this out with the same conviction I brought to the intro post, and didn’t get a result I was confident of.
But the basic “this really should work and yet it doesn’t” sentiment, yeah, I’ve been there as well.
I didn’t asked for u8 arithmetic though.
Zig decided that const a: u32 = worker.group * 64 + worker.local_rank should be computed in u8, even though I precisely communicated my intent of using u32.
I understand both perspectives, and this has bitten me as well. But consider:
const a = @as(u32, worker.group) * 64 + worker.local_rank;
Works now, and keeps things very simple, rather than potentially quite complex.
I wouldn’t have started this thread if I thought every aspect of the current arithmetic system was optimal. But I’m wary of this one, for reasons which don’t unsigned promotion of fairly simple formulas. I could be convinced.
Yes, I think there’s agreement with the sentiment here. Nevertheless, it’s clear to see how one can conclude that if the operands are u8s, then one is “asking for u8 arithmetic”… but it’s easily arguable that if one’s RL is a u27, then one is actually, in a way, “asking for u27 arithmetic”. I guess that’s not obvious.
From a productivity standpoint I find this kind of bug very disruptive because it’s only caught at runtime under specific condition, not visible in the code, and often requires a debugger.
From a math standpoint I don’t see how those checks help. The checks are here to guarantee the hardware is doing the math correctly. When I write a * x - b it has a well defined mathematic meaning that doesn’t care about the “size” of a x or b. But I want to know if the hardware was able to produce the correct result. Therefore the checks depends on the result type.
I think using the input type of each AST node is the lazy choice other languages made but Zig is uniquely positioned to do smth better. And for u1, u2, u4 or u8 it’s almost impossible to do interesting multiplication without leaving their range.
Note my problem would also be solved by the “range“ proposal, but I’m a but worried about the codegen when accidentally introducing u512 operations.
This might sound like more of an edge case than it actually is. I’ll use s for signed and n for uNsigned.
So the statement is:
s1 -= n1 - n2;
The problem is: we might be using unsigned on the RHS because the result always has to be positive. Now the overflow trap is there to keep us out of trouble.
If we let the result location promote the RHS to signed, we lose that, and bad things happen.
Of course there are other ways to get that, assert( n1 >= n2);—but that’s what the subtraction is doing right now, and if additive overflow can subvert expectations, so can failure-to-trap. So we’ve traded one footgun for another, and it’s not totally obvious we’ve gained in trade. Your case may be more common, maybe not but I think so, but my case is a silent failure which won’t panic in safe modes: harder to surface.
In this example, why does n1 - n2 has to be positive? It seems you want to update a signed number? If you’re tracking an average delta of unsigned, it’s probably correct to NOT put the bound check here.
I agree that it’s a big breaking change in the sense that people may rely on currently inserted checks, so moving them can be surprising or wrong and will be hard to debug. It would be interesting to see eg how much std code is impacted.
It’s an invariant of the algorithm. s1 must always be smaller (or the same size) at the end of each iteration, and by using unsigned subtraction, we ‘know this is true’: given how Zig works right now.
It’s only meant to illustrate that sometimes, the bounds dictated by local arithmetic are intended, and if promotion by result location subverts that, then a bug coming in from somewhere else becomes silent instead of loud.