Repository: bytecodealliance/wasmtime
Pull request #7453 (2023-11-02 14:00:47)

egraphs: Merge consecutive shifts and rotates
👋 Hey,

This PR adds a few mid-end rules that merge consecutive shift and rotate instructions. Here's an overview of the rules added.

#### Merge consecutive shifts by a constant
```lisp
(ishl (ishl x k1) k2) == (ishl x (add k1 k2)) if k1 + k2 < ty_bits
(ushr (ushr x k1) k2) == (ushr x (add k1 k2)) if k1 + k2 < ty_bits
(sshr (sshr x k1) k2) == (sshr x (add k1 k2)) if k1 + k2 < ty_bits
```
#### Merge consecutive shifts by a constant if they overflow the type size
```lisp
(ishl (ishl x k1) k2) == 0 if k1 + k2 >= ty_bits
(ushr (ushr x k1) k2) == 0 if k1 + k2 >= ty_bits
```

#### Merge consecutive rotates
```lisp
(rotl (rotr x y) y) == x
(rotr (rotl x y) y) == x

(rotr (rotr x y) z) == (rotr x (iadd y z))
(rotl (rotl x y) z) == (rotl x (iadd y z))
(rotr (rotl x y) z) == (rotr x (isub y z))
(rotl (rotr x y) z) == (rotl x (isub y z))
```

github-actions[bot]: #### Subscribe to Label Action

cc @cfallin, @fitzgen

<details>
This issue or pull request has been labeled: "cranelift", "isle"

Thus the following users have been cc'd because of the following labels:

* cfallin: isle
* fitzgen: isle

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>

Patch:
diff --git a/cranelift/codegen/src/isa/riscv64/inst.isle b/cranelift/codegen/src/isa/riscv64/inst.isle
index 8627f2a29..0c4e1e3b5 100644
--- a/cranelift/codegen/src/isa/riscv64/inst.isle
+++ b/cranelift/codegen/src/isa/riscv64/inst.isle
@@ -1783,9 +1783,6 @@
 
 
 
-;; Generate a mask for the bit-width of the given type
-(decl pure shift_mask (Type) u64)
-(rule (shift_mask ty) (u64_sub (ty_bits (lane_type ty)) 1))
 
 ;; Helper for generating a i64 from a pair of Imm20 and Imm12 constants
 (decl i64_generate_imm (Imm20 Imm12) i64)
diff --git a/cranelift/codegen/src/isa/riscv64/lower.isle b/cranelift/codegen/src/isa/riscv64/lower.isle
index 0d3a271f4..e3fa6a9f2 100644
--- a/cranelift/codegen/src/isa/riscv64/lower.isle
+++ b/cranelift/codegen/src/isa/riscv64/lower.isle
@@ -1193,7 +1193,7 @@
 
 ;; 8/16 bit types need a mask on the shift amount
 (rule 0 (lower (has_type (ty_int (ty_8_or_16 ty)) (ishl x y)))
-  (if-let mask (u64_to_imm12 (shift_mask ty)))
+  (if-let mask (u64_to_imm12 (ty_shift_mask ty)))
   (rv_sllw x (rv_andi (value_regs_get y 0) mask)))
 
 ;; Using the 32bit version of `sll` automatically masks the shift amount.
@@ -1206,12 +1206,12 @@
 
 ;; If the shift amount is known. We can mask it and encode it in the instruction.
 (rule 2 (lower (has_type (int_fits_in_32 ty) (ishl x (maybe_uextend (imm12_from_value y)))))
-  (rv_slliw x (imm12_and y (shift_mask ty))))
+  (rv_slliw x (imm12_and y (ty_shift_mask ty))))
 
 ;; We technically don't need to mask the shift amount here. The instruction
 ;; does the right thing. But it's neater when pretty printing it.
 (rule 3 (lower (has_type ty @ $I64 (ishl x (maybe_uextend (imm12_from_value y)))))
-  (rv_slli x (imm12_and y (shift_mask ty))))
+  (rv_slli x (imm12_and y (ty_shift_mask ty))))
 
 ;; With `Zba` we have a shift that zero extends the LHS argument.
 (rule 4 (lower (has_type $I64 (ishl (uextend x @ (value_type $I32)) (maybe_uextend (imm12_from_value y)))))
@@ -1253,7 +1253,7 @@
 ;; 8/16 bit types need a mask on the shift amount, and the LHS needs to be
 ;; zero extended.
 (rule 0 (lower (has_type (ty_int (fits_in_16 ty)) (ushr x y)))
-  (if-let mask (u64_to_imm12 (shift_mask ty)))
+  (if-let mask (u64_to_imm12 (ty_shift_mask ty)))
   (rv_srlw (zext x) (rv_andi (value_regs_get y 0) mask)))
 
 ;; Using the 32bit version of `srl` automatically masks the shift amount.
@@ -1266,7 +1266,7 @@
 
 ;; When the RHS is known we can just encode it in the instruction.
 (rule 2 (lower (has_type (ty_int (fits_in_16 ty)) (ushr x (maybe_uextend (imm12_from_value y)))))
-  (rv_srliw (zext x) (imm12_and y (shift_mask ty))))
+  (rv_srliw (zext x) (imm12_and y (ty_shift_mask ty))))
 
 (rule 3 (lower (has_type $I32 (ushr x (maybe_uextend (imm12_from_value y)))))
   (rv_srliw x y))
@@ -1308,7 +1308,7 @@
 ;; 8/16 bit types need a mask on the shift amount, and the LHS needs to be
 ;; zero extended.
 (rule 0 (lower (has_type (ty_int (fits_in_16 ty)) (sshr x y)))
-  (if-let mask (u64_to_imm12 (shift_mask ty)))
+  (if-let mask (u64_to_imm12 (ty_shift_mask ty)))
   (rv_sraw (sext x) (rv_andi (value_regs_get y 0) mask)))
 
 ;; Using the 32bit version of `sra` automatically masks the shift amount.
@@ -1321,7 +1321,7 @@
 
 ;; When the RHS is known we can just encode it in the instruction.
 (rule 2 (lower (has_type (ty_int (fits_in_16 ty)) (sshr x (maybe_uextend (imm12_from_value y)))))
-  (rv_sraiw (sext x) (imm12_and y (shift_mask ty))))
+  (rv_sraiw (sext x) (imm12_and y (ty_shift_mask ty))))
 
 (rule 3 (lower (has_type $I32 (sshr x (maybe_uextend (imm12_from_value y)))))
   (rv_sraiw x y))
diff --git a/cranelift/codegen/src/opts/bitops.isle b/cranelift/codegen/src/opts/bitops.isle
index 018db93b6..6c89e0f07 100644
--- a/cranelift/codegen/src/opts/bitops.isle
+++ b/cranelift/codegen/src/opts/bitops.isle
@@ -98,11 +98,11 @@
 ;; (x | -x) sets the sign bit to 1 if x is nonzero, and 0 if x is zero. sshr propagates
 ;; the sign bit to the rest of the value.
 (rule (simplify (sshr ty (bor ty x (ineg ty x)) (iconst ty (u64_from_imm64 shift_amt))))
-      (if-let $true (u64_eq shift_amt (u64_sub (ty_bits ty) 1)))
+      (if-let $true (u64_eq shift_amt (ty_shift_mask ty)))
       (bmask ty x))
 
 (rule (simplify (sshr ty (bor ty (ineg ty x) x) (iconst ty (u64_from_imm64 shift_amt))))
-      (if-let $true (u64_eq shift_amt (u64_sub (ty_bits ty) 1)))
+      (if-let $true (u64_eq shift_amt (ty_shift_mask ty)))
       (bmask ty x))
 
 ;; Matches any expressions that preserve "truthiness".
diff --git a/cranelift/codegen/src/opts/shifts.isle b/cranelift/codegen/src/opts/shifts.isle
index 1b33d89d8..acf53946c 100644
--- a/cranelift/codegen/src/opts/shifts.isle
+++ b/cranelift/codegen/src/opts/shifts.isle
@@ -100,7 +100,7 @@
 
 ;; ineg(ushr(x, k)) == sshr(x, k) when k == ty_bits - 1.
 (rule (simplify (ineg ty (ushr ty x sconst @ (iconst ty (u64_from_imm64 shift_amt)))))
-      (if-let $true (u64_eq shift_amt (u64_sub (ty_bits ty) 1)))
+      (if-let $true (u64_eq shift_amt (ty_shift_mask ty)))
       (sshr ty x sconst))
 
 ;; Shifts and rotates allow a different type for the shift amount, so we
@@ -144,3 +144,127 @@
 (rule (simplify (sshr ty x (iconcat _ y _))) (sshr ty x y))
 (rule (simplify (rotr ty x (iconcat _ y _))) (rotr ty x y))
 (rule (simplify (rotl ty x (iconcat _ y _))) (rotl ty x y))
+
+;; Try to combine the shift amount from multiple consecutive shifts
+;; This only works if the shift amount remains smaller than the bit
+;; width of the type.
+;;
+;; (ishl (ishl x k1) k2) == (ishl x (add k1 k2)) if shift_mask(k1) + shift_mask(k2) < ty_bits
+;; (ushr (ushr x k1) k2) == (ushr x (add k1 k2)) if shift_mask(k1) + shift_mask(k2) < ty_bits
+;; (sshr (sshr x k1) k2) == (sshr x (add k1 k2)) if shift_mask(k1) + shift_mask(k2) < ty_bits
+(rule (simplify (ishl ty
+                      (ishl ty x (iconst kty (u64_from_imm64 k1)))
+                      (iconst _ (u64_from_imm64 k2))))
+      (if-let shift_amt (u64_add
+                              (u64_and k1 (ty_shift_mask ty))
+                              (u64_and k2 (ty_shift_mask ty))))
+      (if-let $true (u64_lt shift_amt (ty_bits_u64 (lane_type ty))))
+      (ishl ty x (iconst_u64 kty shift_amt)))
+
+(rule (simplify (ushr ty
+                      (ushr ty x (iconst kty (u64_from_imm64 k1)))
+                      (iconst _ (u64_from_imm64 k2))))
+      (if-let shift_amt (u64_add
+                              (u64_and k1 (ty_shift_mask ty))
+                              (u64_and k2 (ty_shift_mask ty))))
+      (if-let $true (u64_lt shift_amt (ty_bits_u64 (lane_type ty))))
+      (ushr ty x (iconst_u64 kty shift_amt)))
+
+(rule (simplify (sshr ty
+                      (sshr ty x (iconst kty (u64_from_imm64 k1)))
+                      (iconst _ (u64_from_imm64 k2))))
+      (if-let shift_amt (u64_add
+                              (u64_and k1 (ty_shift_mask ty))
+                              (u64_and k2 (ty_shift_mask ty))))
+      (if-let $true (u64_lt shift_amt (ty_bits_u64 (lane_type ty))))
+      (sshr ty x (iconst_u64 kty shift_amt)))
+
+;; Simliarly, if the shift amount overflows the type, then we can turn
+;; it into a 0
+;;
+;; (ishl (ishl x k1) k2) == 0 if shift_mask(k1) + shift_mask(k2) >= ty_bits
+;; (ushr (ushr x k1) k2) == 0 if shift_mask(k1) + shift_mask(k2) >= ty_bits
+(rule (simplify (ishl ty
+                      (ishl ty x (iconst _ (u64_from_imm64 k1)))
+                      (iconst _ (u64_from_imm64 k2))))
+      (if-let shift_amt (u64_add
+                              (u64_and k1 (ty_shift_mask ty))
+                              (u64_and k2 (ty_shift_mask ty))))
+      (if-let $true (u64_le (ty_bits_u64 ty) shift_amt))
+      (subsume (iconst_u64 ty 0)))
+
+(rule (simplify (ushr ty
+                      (ushr ty x (iconst _ (u64_from_imm64 k1)))
+                      (iconst _ (u64_from_imm64 k2))))
+      (if-let shift_amt (u64_add
+                              (u64_and k1 (ty_shift_mask ty))
+                              (u64_and k2 (ty_shift_mask ty))))
+      (if-let $true (u64_le (ty_bits_u64 ty) shift_amt))
+      (subsume (iconst_u64 ty 0)))
+
+;; (rotl (rotr x y) y) == x
+;; (rotr (rotl x y) y) == x
+(rule (simplify (rotl ty (rotr ty x y) y)) (subsume x))
+(rule (simplify (rotr ty (rotl ty x y) y)) (subsume x))
+
+;; Emits an iadd for two values. If they have different types
+;; then the smaller type is zero extended to the larger type.
+(decl iadd_uextend (Value Value) Value)
+(rule 1 (iadd_uextend x @ (value_type ty) y @ (value_type ty))
+      (iadd ty x y))
+(rule 2 (iadd_uextend x @ (value_type x_ty) y @ (value_type y_ty))
+      (if-let $true (u64_lt (ty_bits_u64 x_ty) (ty_bits_u64 y_ty)))
+      (iadd y_ty (uextend y_ty x) y))
+(rule 3 (iadd_uextend x @ (value_type x_ty) y @ (value_type y_ty))
+      (if-let $true (u64_lt (ty_bits_u64 y_ty) (ty_bits_u64 x_ty)))
+      (iadd x_ty x (uextend x_ty y)))
+
+;; Emits an isub for two values. If they have different types
+;; then the smaller type is zero extended to the larger type.
+(decl isub_uextend (Value Value) Value)
+(rule 1 (isub_uextend x @ (value_type ty) y @ (value_type ty))
+      (isub ty x y))
+(rule 2 (isub_uextend x @ (value_type x_ty) y @ (value_type y_ty))
+      (if-let $true (u64_lt (ty_bits_u64 x_ty) (ty_bits_u64 y_ty)))
+      (isub y_ty (uextend y_ty x) y))
+(rule 3 (isub_uextend x @ (value_type x_ty) y @ (value_type y_ty))
+      (if-let $true (u64_lt (ty_bits_u64 y_ty) (ty_bits_u64 x_ty)))
+      (isub x_ty x (uextend x_ty y)))
+
+;; Try to group constants together so that other cprop rules can optimize them.
+;;
+;; (rotr (rotr x y) z) == (rotr x (iadd y z))
+;; (rotl (rotl x y) z) == (rotl x (iadd y z))
+;; (rotr (rotl x y) z) == (rotr x (isub y z))
+;; (rotl (rotr x y) z) == (rotl x (isub y z))
+;;
+;; if x or z are constants
+(rule (simplify (rotl ty (rotl ty x y @ (iconst _ _)) z)) (rotl ty x (iadd_uextend y z)))
+(rule (simplify (rotl ty (rotl ty x y) z @ (iconst _ _))) (rotl ty x (iadd_uextend y z)))
+(rule (simplify (rotr ty (rotr ty x y @ (iconst _ _)) z)) (rotr ty x (iadd_uextend y z)))
+(rule (simplify (rotr ty (rotr ty x y) z @ (iconst _ _))) (rotr ty x (iadd_uextend y z)))
+
+(rule (simplify (rotr ty (rotl ty x y @ (iconst _ _)) z)) (rotl ty x (isub_uextend y z)))
+(rule (simplify (rotr ty (rotl ty x y) z @ (iconst _ _))) (rotl ty x (isub_uextend y z)))
+(rule (simplify (rotl ty (rotr ty x y @ (iconst _ _)) z)) (rotr ty x (isub_uextend y z)))
+(rule (simplify (rotl ty (rotr ty x y) z @ (iconst _ _))) (rotr ty x (isub_uextend y z)))
+
+;; Similarly to the rules above, if y and z have the same type, we should emit
+;; an iadd or isub instead. In some backends this is cheaper than a rotate.
+;;
+;; If they have different types we end up in a situation where we have to insert
+;; and additional extend and that transformation is not universally beneficial.
+;;
+;; (rotr (rotr x y) z) == (rotr x (iadd y z))
+;; (rotl (rotl x y) z) == (rotl x (iadd y z))
+;; (rotr (rotl x y) z) == (rotl x (isub y z))
+;; (rotl (rotr x y) z) == (rotr x (isub y z))
+(rule (simplify (rotr ty (rotr ty x y @ (value_type kty)) z @ (value_type kty)))
+      (rotr ty x (iadd_uextend y z)))
+(rule (simplify (rotl ty (rotl ty x y @ (value_type kty)) z @ (value_type kty)))
+      (rotl ty x (iadd_uextend y z)))
+
+(rule (simplify (rotr ty (rotl ty x y @ (value_type kty)) z @ (value_type kty)))
+      (rotl ty x (isub_uextend y z)))
+(rule (simplify (rotl ty (rotr ty x y @ (value_type kty)) z @ (value_type kty)))
+      (rotr ty x (isub_uextend y z)))
diff --git a/cranelift/codegen/src/prelude.isle b/cranelift/codegen/src/prelude.isle
index eb03dd24a..c61876b8d 100644
--- a/cranelift/codegen/src/prelude.isle
+++ b/cranelift/codegen/src/prelude.isle
@@ -323,6 +323,10 @@
 (decl pure partial ty_half_width (Type) Type)
 (extern constructor ty_half_width ty_half_width)
 
+;; Generate a mask for the maximum shift amount for a given type. i.e 31 for I32.
+(decl pure ty_shift_mask (Type) u64)
+(rule (ty_shift_mask ty) (u64_sub (ty_bits (lane_type ty)) 1))
+
 ;; Compare two types for equality.
 (decl pure ty_equal (Type Type) bool)
 (extern constructor ty_equal ty_equal)
diff --git a/cranelift/codegen/src/prelude_opt.isle b/cranelift/codegen/src/prelude_opt.isle
index 099930f5b..921c7c65b 100644
--- a/cranelift/codegen/src/prelude_opt.isle
+++ b/cranelift/codegen/src/prelude_opt.isle
@@ -43,3 +43,16 @@
 ;; answer".
 (decl subsume (Value) Value)
 (extern constructor subsume subsume)
+
+;;;;; constructors ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(decl iconst_u64 (Type u64) Value)
+
+;; Use a single iconst for types that fit in 64 bits.
+(rule 0 (iconst_u64 (ty_int_ref_scalar_64 ty) val)
+    (if-let $true (u64_le val (ty_umax ty)))
+    (iconst ty (imm64_masked ty val)))
+
+;; For i128 types use a iconst, but zero extend it to i128.
+(rule 1 (iconst_u64 $I128 val)
+    (uextend $I128 (iconst $I64 (imm64_masked $I64 val))))


Test patch:
diff --git a/cranelift/filetests/filetests/egraph/shifts.clif b/cranelift/filetests/filetests/egraph/shifts.clif
index 7852ada12..dbe984c84 100644
--- a/cranelift/filetests/filetests/egraph/shifts.clif
+++ b/cranelift/filetests/filetests/egraph/shifts.clif
@@ -524,3 +524,237 @@ block0(v0: i8, v1: i16, v2: i16):
 
 ; check: v5 = rotl v0, v1
 ; check: return v5
+
+function %ishl_prop_overflow(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i8 7
+    v2 = iconst.i8 7
+    v3 = ishl.i8 v0, v1
+    v4 = ishl.i8 v3, v2
+    return v4
+}
+
+; check: v5 = iconst.i8 0
+; check: return v5  ; v5 = 0
+
+function %ishl_prop_type_diff(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i16 7
+    v2 = iconst.i16 7
+    v3 = ishl.i8 v0, v1
+    v4 = ishl.i8 v3, v2
+    return v4
+}
+
+; check: v5 = iconst.i8 0
+; check: return v5  ; v5 = 0
+
+function %ushr_prop(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i8 1
+    v2 = iconst.i8 1
+    v3 = ushr.i8 v0, v1
+    v4 = ushr.i8 v3, v2
+    return v4
+}
+
+; check: v5 = iconst.i8 2
+; check: v6 = ushr v0, v5  ; v5 = 2
+; check: return v6
+
+function %ushr_prop_overflow(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i8 7
+    v2 = iconst.i8 7
+    v3 = ushr.i8 v0, v1
+    v4 = ushr.i8 v3, v2
+    return v4
+}
+
+; check: v5 = iconst.i8 0
+; check: return v5  ; v5 = 0
+
+
+function %sshr_prop(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i8 1
+    v2 = iconst.i8 1
+    v3 = sshr.i8 v0, v1
+    v4 = sshr.i8 v3, v2
+    return v4
+}
+
+; check: v5 = iconst.i8 2
+; check: v6 = sshr v0, v5  ; v5 = 2
+; check: return v6
+
+function %i128_ushr_overflow_becomes_0(i128) -> i128 {
+block0(v0: i128):
+    v1 = iconst.i16 200
+    v2 = ushr v0, v1
+    v3 = ushr v2, v1
+    return v3
+}
+
+; check: v4 = iconst.i64 0
+; nextln: v5 = uextend.i128 v4  ; v4 = 0
+; nextln: return v5
+
+
+function %i128_ishl_overflow_becomes_0(i128) -> i128 {
+block0(v0: i128):
+    v1 = iconst.i16 200
+    v2 = ishl v0, v1
+    v3 = ishl v2, v1
+    return v3
+}
+
+; check: v4 = iconst.i64 0
+; nextln: v5 = uextend.i128 v4  ; v4 = 0
+; nextln: return v5
+
+function %simd_shift_does_not_panic(i8x16) -> i8x16 {
+block0(v0: i8x16):
+    v1 = iconst.i64 0
+    v2 = ushr v0, v1
+    v3 = ushr v2, v1
+    return v3
+}
+
+; check: v1 = iconst.i64 0
+; nextln: v2 = ushr v0, v1  ; v1 = 0
+; check: return v2
+
+function %merges_shift_amount_based_on_lane_type(i8x16) -> i8x16 {
+block0(v0: i8x16):
+    v1 = iconst.i16 30
+    v2 = iconst.i16 3
+
+    v3 = sshr v0, v1
+    v4 = sshr v3, v2
+    v5 = sshr v4, v2
+    return v5
+}
+
+; check: v1 = iconst.i16 30
+; nextln: v3 = sshr v0, v1  ; v1 = 30
+; nextln: v6 = iconst.i16 6
+; nextln: v7 = sshr v3, v6  ; v6 = 6
+; check: return v7
+
+
+function %rotr_rotr_prop(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i8 1
+    v2 = iconst.i8 1
+    v3 = rotr.i8 v0, v1
+    v4 = rotr.i8 v3, v2
+    return v4
+}
+
+; check: v11 = iconst.i8 2
+; check: v13 = rotr v0, v11  ; v11 = 2
+; check: return v13
+
+function %rotr_rotr_add(i8, i8, i8) -> i8 {
+block0(v0: i8, v1: i8, v2: i8):
+    v3 = rotr.i8 v0, v1
+    v4 = rotr.i8 v3, v2
+    return v4
+}
+
+; check: v5 = iadd v1, v2
+; check: v6 = rotr v0, v5
+; check: return v6
+
+function %rotl_rotl_prop(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i8 1
+    v2 = iconst.i8 1
+    v3 = rotl.i8 v0, v1
+    v4 = rotl.i8 v3, v2
+    return v4
+}
+
+; check: v11 = iconst.i8 2
+; check: v13 = rotl v0, v11  ; v11 = 2
+; check: return v13
+
+function %rotl_rotl_add(i8, i8, i8) -> i8 {
+block0(v0: i8, v1: i8, v2: i8):
+    v3 = rotl.i8 v0, v1
+    v4 = rotl.i8 v3, v2
+    return v4
+}
+
+; check: v5 = iadd v1, v2
+; check: v6 = rotl v0, v5
+; check: return v6
+
+
+function %rotl_rotr_prop(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i8 2
+    v2 = iconst.i8 1
+    v3 = rotr.i8 v0, v1
+    v4 = rotl.i8 v3, v2
+    return v4
+}
+
+; check: v2 = iconst.i8 1
+; check: v21 = rotr v0, v2  ; v2 = 1
+; check: return v21
+
+function %rotl_rotr_add(i8, i8, i8) -> i8 {
+block0(v0: i8, v1: i8, v2: i8):
+    v3 = rotr.i8 v0, v1
+    v4 = rotl.i8 v3, v2
+    return v4
+}
+
+; check: v5 = isub v1, v2
+; check: v6 = rotr v0, v5
+; check: return v6
+
+function %rotl_rotr_prop(i8) -> i8 {
+block0(v0: i8):
+    v1 = iconst.i8 2
+    v2 = iconst.i8 1
+    v3 = rotl.i8 v0, v1
+    v4 = rotr.i8 v3, v2
+    return v4
+}
+
+; check: v2 = iconst.i8 1
+; check: v21 = rotl v0, v2  ; v2 = 1
+; check: return v21
+
+function %rotl_rotr_add(i8, i8, i8) -> i8 {
+block0(v0: i8, v1: i8, v2: i8):
+    v3 = rotl.i8 v0, v1
+    v4 = rotr.i8 v3, v2
+    return v4
+}
+
+; check: v5 = isub v1, v2
+; check: v6 = rotl v0, v5
+; check: return v6
+
+function %rotl_rotr_subsume(i8, i8) -> i8 {
+block0(v0: i8, v1: i8):
+    v2 = rotl.i8 v0, v1
+    v3 = rotr.i8 v2, v1
+    return v3
+}
+
+; check: return v0
+
+
+function %rotr_rotl_subsume(i8, i8) -> i8 {
+block0(v0: i8, v1: i8):
+    v2 = rotr.i8 v0, v1
+    v3 = rotl.i8 v2, v1
+    return v3
+}
+
+; check: return v0