RFA-342 · Case file with fixtures · Case 314 of 694 · Runtime evidence
wrapping_shl Masks the Shift Count in Rust
wrapping_shl wraps the right-hand shift amount to the integer bit width. It does not compute an unlimited-width shift and then keep the low bits, so callers needing validation should use checked_shl.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- wrapping_shl masks the right-hand count modulo the integer bit width rather than performing an infinite-precision shift followed by truncation.
- First discriminating check
- Build a boundary table for counts 31, 32, and 33, then use checked_shl when a count outside the type width must remain invalid.
I once used wrapping_shl while decoding a compact bit field. I wanted overflow in the value to wrap, but I also assumed a shift by the full width would move every bit away and produce zero. For i32, shifting 42 by 32 returns 42.
The failing program contains only that assumption. Its failure is useful because the method name says “wrapping,” but two different things could wrap.
The right-hand operand is what wraps
i32::wrapping_shl masks the shift count so it stays within the number of bits in the type. For a 32-bit integer, count 32 becomes zero, 33 becomes one, and so on.
Therefore 42.wrapping_shl(32) is the same operation as shifting by zero. It returns 42. Shifting by 33 returns 84.
This is not the model of first creating an infinitely wide value, moving the bits left, and truncating the result back to 32 bits. Under that model, a shift by 32 would indeed leave no low bits. The standard method documents that its semantics differ.
Value overflow and count overflow are separate
For a permitted shift count, left shift may discard high bits from the fixed-width value. A wrapping operation accepts that fixed-width result. Separately, the count itself may be at least the bit width.
wrapping_shl defines the second case by reducing the count. This is convenient for rotations, low-level instruction modelling, hash mixing, and algorithms whose shift amount is intentionally modulo the word size.
It is usually wrong for input validation. A network field saying “shift by 32” should not quietly mean “shift by zero” unless the protocol explicitly specifies modulo arithmetic.
checked_shl preserves invalidity
The repaired program demonstrates both contracts. It asserts the modulo results, then uses checked_shl to receive None for count 32.
I prefer checked_shl at parsing, configuration, and user-input boundaries. The Option forces the code to decide whether a large count is an error, a default, or a value that deserves another representation.
Once an internal algorithm has proved the range or explicitly needs modulo behavior, wrapping_shl can state that decision clearly.
The shift operator has its own failure context
The Rust Reference lists shifts among arithmetic and logical binary operators. Overflow checking and the generated behavior of << can depend on whether the right operand is known and valid. I do not use a plain operator as an implicit substitute for choosing checked or wrapping semantics.
Named integer methods are useful during review because they state the policy at the operation. checked_shl, wrapping_shl, and related methods do not merely avoid a panic; they describe different domains.
I also keep the count type and range visible. Casting a negative signed value to an unsigned shift count before validation can turn one invalid state into a very large number, which wrapping then hides.
A tiny truth table prevents intuition bugs
For a 32-bit value I test counts zero, one, 31, 32, 33, and a much larger number. I include zero, a positive value with high bits set, and a negative signed value when the application uses signed integers.
Testing only count one proves almost nothing about the boundary. Testing only zero and 31 still misses exactly where the method's modulo policy starts.
When porting bit code from C, Java, a VM instruction set, or a file format, I write the expected table from that specification first. Similar spelling does not guarantee identical shift-count semantics.
Rotations are not wrapping shifts
A rotate preserves bits by moving those that leave one side back into the other. A wrapping left shift discards shifted-out bits; it only wraps the count. These operations coincide for some values and counts but are not interchangeable.
If I mean rotation, I call rotate_left. If I mean a fixed-width shift with modulo count, I call wrapping_shl. If an out-of-range count is invalid, I call checked_shl. Naming the policy removes a surprising amount of debugging.
The core principle is to choose the domain
“Overflow” is too broad a word. The value may exceed its range, the shift count may exceed its range, a conversion may narrow, or an intermediate mathematical result may not fit. Each Rust API chooses a specific answer for a specific boundary.
I now ask which quantity is wrapping before accepting a method because its name sounds safe. In wrapping_shl, the count is reduced modulo the bit width. That one sentence is the contract, and the fixture keeps it executable.