]> git.lizzy.rs Git - rust.git/commitdiff
explain why shift with signed offset works the way it does
authorRalf Jung <post@ralfj.de>
Sun, 6 Mar 2022 03:47:31 +0000 (22:47 -0500)
committerRalf Jung <post@ralfj.de>
Sun, 6 Mar 2022 16:29:24 +0000 (11:29 -0500)
compiler/rustc_const_eval/src/interpret/operator.rs

index 079ce9f07b8e10f5e322ec4c03add05fc224dfec..6dae9dc72b7b4668df706aa5dc605f0e2c3a7ea4 100644 (file)
@@ -127,17 +127,29 @@ fn binary_int_op(
 
         // Shift ops can have an RHS with a different numeric type.
         if bin_op == Shl || bin_op == Shr {
-            let signed = left_layout.abi.is_signed();
             let size = u128::from(left_layout.size.bits());
+            // Even if `r` is signed, we treat it as if it was unsigned (i.e., we use its
+            // zero-extended form). This matches the codegen backend:
+            // <https://github.com/rust-lang/rust/blob/c274e4969f058b1c644243181ece9f829efa7594/compiler/rustc_codegen_ssa/src/base.rs#L315-L317>.
+            // The overflow check is also ignorant to the sign:
+            // <https://github.com/rust-lang/rust/blob/c274e4969f058b1c644243181ece9f829efa7594/compiler/rustc_codegen_ssa/src/mir/rvalue.rs#L728>.
+            // This would behave rather strangely if we had integer types of size 256: a shift by
+            // -1i8 would actually shift by 255, but that would *not* be considered overflowing. A
+            // shift by -1i16 though would be considered overflowing. If we had integers of size
+            // 512, then a shift by -1i8 would even produce a different result than one by -1i16:
+            // the first shifts by 255, the latter by u16::MAX % 512 = 511. Lucky enough, our
+            // integers are maximally 128bits wide, so negative shifts *always* overflow and we have
+            // consistent results for the same value represented at different bit widths.
+            assert!(size <= 128);
             let overflow = r >= size;
             // The shift offset is implicitly masked to the type size, to make sure this operation
             // is always defined. This is the one MIR operator that does *not* directly map to a
             // single LLVM operation. See
-            // <https://github.com/rust-lang/rust/blob/a3b9405ae7bb6ab4e8103b414e75c44598a10fd2/compiler/rustc_codegen_ssa/src/common.rs#L131-L158>
+            // <https://github.com/rust-lang/rust/blob/c274e4969f058b1c644243181ece9f829efa7594/compiler/rustc_codegen_ssa/src/common.rs#L131-L158>
             // for the corresponding truncation in our codegen backends.
             let r = r % size;
             let r = u32::try_from(r).unwrap(); // we masked so this will always fit
-            let result = if signed {
+            let result = if left_layout.abi.is_signed() {
                 let l = self.sign_extend(l, left_layout) as i128;
                 let result = match bin_op {
                     Shl => l.checked_shl(r).unwrap(),