-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Explicit Shift Operators #5220
Comments
RISC-V has logical left shift, logical right shift, and arithmetic right shift. I don't think having one of our operators suddenly not map to a machine instruction is necessarily a good idea. Maybe we could distinguish between logical and arithmetic right shift, but I don't agree with the rest of this proposal. |
I don't think we should make decisions based on the presence or absence of hardware instructions. |
The two variants of left shift are mathematically equivalent, since the sign bit doesn't shift into anything. That's why RISC-V only has one instruction for them. Both map to the same machine instruction. |
Oh wait, I didn't read carefully enough. The proposal is suggesting filling with the least significant bit. Is filling with the lsb actually a useful operation? Most ISAs don't support anything like that, so unless there's a compelling argument that it's a useful primitive for the language to define I don't think it should be included. |
All |
@SpexGuy @EleanorNB I actually have no particularly strong technical opinion on whether the The primary use case I imagine is programmatically creating a right-aligned mask of width Not unlike non-native bitwidth integers, the strength of its argument is more a matter of its convenience, and therefore ease of correctness, rather than technical necessity. |
Normally, a right-aligned mask of There may be some value in separating logical and arithmetic shift into separate operators. But we have slightly different concerns from Java. Java requires a separate operator because it doesn't have unsigned integer types (for the most part). And it's "default" right shift is sign extending. So we need to consider how it would fit into Zig. Personally, I feel that shifting zeroes into the top bits of a negative signed integer is extremely unexpected behavor. Then again, shifting a signed integer left also totally clobbers the sign bit, so it's also unexpected. Most hardware has a way to do these things, so we should have some support for them, but maybe it shouldn't be front and center. It seems like we're kind of in a "damned if you do, damned if you don't" scenario. As you pointed out, using the type to determine what kind of shift to do is a bit of a footgun, especially with Zig's inferred types on everything. But if we break convention from C and Rust and make the
With these changes, it's difficult to do the wrong thing accidentally. When shifting unsigned integers, sign extending makes no sense so it's obviously shifting zeroes in. When shifting signed integers, they either have to be cast to unsigned or use the explicit I don't think we should support |
Left-shifting into the sign bit is actually the most sensible option -- it represents the multiplication truncated to the range of the type, and it's easy to check for overflow by watching for a sign change. |
I support this proposal, it's often confusing and error-prone to have arithmetic and logic shift glued to the type. Shifts are not arithmetics and should that should be respected. If i want to divide, i can write |
agreed with the general idea, (especially
saturation on shifts? .... i have no idea. |
Explicit Shift Operators
This proposal suggests making bit-shifting operations explicit and type-agnostic.
But why?
Currently, the behavior of
>>
andshrExact
depend on the user knowing the type being worked on.which outputs:
Needing to know the type to know what an operator does is a common argument against operator overloading, so by this logic Zig's current shr's behavior is inconsistent with it's opinions.
Furthermore, this implicit operator overloading makes it difficult to port shift-heavy bitwise algorithms between signed and unsigned types, as the behavior is now completely different. For example, take this conversation between intellectual gentlemen:
The amount of ignorance and confusion in this conversation spans multiple dimensions. We could be smug about knowing arcane rules, but there's clearly something wrong about this simple operator. They almost halved their throughput! There is unnecessary cognitive overhead when working with right-shift.
Ergo! We take (the only?) good idea from a certain other language and break them into separate operators, with some differences.
"We should toss in some more operators, it responded well to that."
In Zig, there are four bitshifting operators.
<<
and>>
operators are 'zero-flooding', i.e, they move the bit-buffer in a direction and leave 0's in its wake. However, it is sometimes convenient to use the 'sticky' bitshifts, which fills its wake with the value of the leftmost bit (in>>>
) or rightmost bit (in<<<
).Bitshifts are sometimes used to simulate arithmetic operations using simpler hardware. For example, in some situations, left-shifting a number is equivalent to multiplying it by a power of 2. However, this is only consistent for unsigned
integersfixedpoints, as the left-shift may occupy the Two's Complement's MSB:Similarly, a right shift may, in some situations, be used to divide by a power of 2, but this is only consistent for unsigned
integersfixedpoints, as shifted signed numbers do not follow division's rounding rules:For this reason, manually representing multiplication or division via shifts is a choice one should consider very carefully. Keep in mind the compiler will automatically convert multiplies and divides to shifts whenever it has enough information to do so.
Wiggling, Pulsating Innards
Implementation is fairly straightforward, but some gotchas are likely to arise.
Drawbacks
Rationale and alternatives
C's shifting is defined as follows:
This undefined and implementation-defined behavior should be taken in context with the era:
So, for one's complement machines, having
n << p
andn >> p
be synonymous withn * 2^p
andn / 2^p
is sensible for both signed and unsigned numbers. In two's complement, this is no longer sensible. C's indecisive solution is to essentially allow the hardware engineers to overload the<<
and>>
operators.By definition, we can not rely on this. Other languages have tried to imitate C's indecisiveness in this matter, allowing language behavior to be defined by historical curiosity and ambivalent waffling.
By defining separate operators for shifts, in terms of zero vs sticky, we sidestep the arithmetic context altogether and treat them exactly as they are: bitwise operators that move and flood. Any arithmetic context is a convenient side-effect.
Alternative Implementation: Chop off its hands
We could have only two bitshift operators,
<<
and>>
, and remove sticky shifting altogether, requiring any equivalent functionality to be recreated using~(mask << a)
and~(mask >> a)
. However this is less desirable, as the mask created by sticky shifting is conditional on the MSB or LSB of the bit-buffer, requiring a rewrite to involve aif / else
branch, which is much less zen-inducing than<<<
and>>>
Prior art
Unresolved questions
Does defining shifts in terms of "Flood-zero's" and "Sticky" make users expect a "Flood-one's" operator? Does the lack of one make programming less convenient?
Future possibilities
Shifts have always been in a strange situation, not quite a bitwise operation, not quite an arithmetic one, and weighed down by historic waffling in computing hardware. We are in a position to clarify the purpose of these fundamental tools, decreasing the number of dumb bugs people deal with every day, and increasing their Zen of Zig, one bit at a time.
The text was updated successfully, but these errors were encountered: