Skip to content

Commit 5c06cf6

Browse files
tx-tomcatjamedzungtnowacki
authored
[Linter] Combinable bool conditions (#16479)
## Description The rules detects and warns about comparison operations in Move code that can be simplified. It identifies comparisons over the same operands joined with a boolean operation, and suggests how to collapse them to a single operation. # Testing - New tests added ## Release notes - [ ] Protocol: - [ ] Nodes (Validators and Full nodes): - [ ] Indexer: - [ ] JSON-RPC: - [ ] GraphQL: - [X] CLI: Move will now lint against pairs of comparison operations that can be combined to a single comparison. - [ ] Rust SDK: --------- Co-authored-by: jamedzung <[email protected]> Co-authored-by: Todd Nowacki <[email protected]>
1 parent 939448c commit 5c06cf6

8 files changed

+1694
-5
lines changed

external-crates/move/crates/move-compiler/src/cfgir/visitor.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,10 @@ pub fn same_value_exp(e1: &H::Exp, e2: &H::Exp) -> bool {
949949
pub fn same_value_exp_(e1: &H::UnannotatedExp_, e2: &H::UnannotatedExp_) -> bool {
950950
use H::UnannotatedExp_ as E;
951951
match (e1, e2) {
952+
(E::Dereference(e) | E::Freeze(e), other) | (other, E::Dereference(e) | E::Freeze(e)) => {
953+
same_value_exp_(&e.exp.value, other)
954+
}
955+
952956
(E::Value(v1), E::Value(v2)) => v1 == v2,
953957
(E::Unit { .. }, E::Unit { .. }) => true,
954958
(E::Constant(c1), E::Constant(c2)) => c1 == c2,
@@ -965,9 +969,6 @@ pub fn same_value_exp_(e1: &H::UnannotatedExp_, e2: &H::UnannotatedExp_) -> bool
965969
.all(|(e1, e2)| same_value_exp(e1, e2))
966970
}
967971

968-
(E::Dereference(e) | E::Freeze(e), other) | (other, E::Dereference(e) | E::Freeze(e)) => {
969-
same_value_exp_(&e.exp.value, other)
970-
}
971972
(E::UnaryExp(op1, e1), E::UnaryExp(op2, e2)) => op1 == op2 && same_value_exp(e1, e2),
972973
(E::BinopExp(l1, op1, r1), E::BinopExp(l2, op2, r2)) => {
973974
op1 == op2 && same_value_exp(l1, l2) && same_value_exp(r1, r2)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
// Copyright (c) The Move Contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! The `CombinableBool` detects and warns about boolean conditions in Move code that can be simplified.
5+
//! It identifies comparisons that are logically equivalent and suggests more concise alternatives.
6+
//! This rule focuses on simplifying expressions involving `==`, `<`, `>`, and `!=` operators to improve code readability.
7+
8+
use crate::{
9+
diag,
10+
linters::StyleCodes,
11+
parser::ast::{BinOp, BinOp_},
12+
typing::{
13+
ast::{self as T},
14+
visitor::{same_value_exp, simple_visitor},
15+
},
16+
};
17+
18+
#[derive(Debug, Clone, Copy)]
19+
enum Simplification {
20+
Reducible(CmpOp),
21+
AlwaysTrue,
22+
AlwaysFalse,
23+
}
24+
25+
#[derive(Debug, Clone, Copy)]
26+
enum BoolOp {
27+
And,
28+
Or,
29+
}
30+
31+
/// See `simplify` for how these values are used.
32+
#[repr(u8)]
33+
#[derive(Debug, Clone, Copy)]
34+
enum CmpOp {
35+
Lt = LT,
36+
Eq = EQ,
37+
Le = LE,
38+
Gt = GT,
39+
Neq = NEQ,
40+
Ge = GE,
41+
}
42+
43+
// See `simplify` for how these values are used.
44+
const FALSE: u8 = 0b000;
45+
const LT: u8 = 0b001;
46+
const EQ: u8 = 0b010;
47+
const LE: u8 = 0b011;
48+
const GT: u8 = 0b100;
49+
const NEQ: u8 = 0b101;
50+
const GE: u8 = 0b110;
51+
const TRUE: u8 = 0b111;
52+
53+
simple_visitor!(
54+
CombinableComparisons,
55+
fn visit_exp_custom(&mut self, exp: &T::Exp) -> bool {
56+
use T::UnannotatedExp_ as E;
57+
let E::BinopExp(outer_l, outer_bop, _, outer_r) = &exp.exp.value else {
58+
return false;
59+
};
60+
// TODO handle negation
61+
let E::BinopExp(l1, op_l, _, r1) = &outer_l.exp.value else {
62+
return false;
63+
};
64+
let E::BinopExp(l2, op_r, _, r2) = &outer_r.exp.value else {
65+
return false;
66+
};
67+
let Some((outer, inner_l, inner_r)) = binop_case(outer_bop, l1, op_l, r1, l2, op_r, r2)
68+
else {
69+
return false;
70+
};
71+
let simplification = simplify(outer, inner_l, inner_r);
72+
let msg = match simplification {
73+
Simplification::Reducible(inner_op) => {
74+
format!("simplifies to the operation '{}'", inner_op)
75+
}
76+
Simplification::AlwaysTrue => "is always 'true'".to_string(),
77+
Simplification::AlwaysFalse => "is always 'false'".to_string(),
78+
};
79+
self.reporter.add_diag(diag!(
80+
StyleCodes::CombinableComparisons.diag_info(),
81+
(exp.exp.loc, format!("This comparison {msg}")),
82+
));
83+
84+
false
85+
}
86+
);
87+
88+
/// Each binary operator is represented as a 3-bit number where each bit represents a range of
89+
/// possible values. With three bits, 0bGEL we are "drawing" an interval of ranges. The comparison
90+
/// `true` if the value is within the interval. so for `x cmp y``
91+
/// ```text
92+
/// G E L
93+
/// ^ this bit represents x < y (less than the equal bit)
94+
/// ^ this bit represents x == y (the equal bit)
95+
/// ^ this bit represents x > y (greater than the equal bit)
96+
/// ```
97+
/// We then take the disjunction of intervals by the bits--creating a bitset.
98+
/// So for example, `>=` is 0b110 since the interval is either greater OR equal.
99+
/// And for `!=` is 0b101 since the interval is either not equal OR less than. We are only dealing
100+
/// with primitives so we know the values are well ordered.
101+
/// From there we can then bitwise-or the bits (set union) when the outer operation is `||` and
102+
/// bitwise-and the bits (set intersection) when the outer operation is `&&` to get the final
103+
/// "simplified" operation. If all bits are set, then the operation is always true. If no bits are
104+
/// set, then the operation is always false.
105+
fn simplify(outer: BoolOp, inner_l: CmpOp, inner_r: CmpOp) -> Simplification {
106+
let lbits = inner_l as u8;
107+
let rbits = inner_r as u8;
108+
let simplification = match outer {
109+
BoolOp::And => lbits & rbits,
110+
BoolOp::Or => lbits | rbits,
111+
};
112+
match simplification {
113+
FALSE => Simplification::AlwaysFalse,
114+
LT => Simplification::Reducible(CmpOp::Lt),
115+
EQ => Simplification::Reducible(CmpOp::Eq),
116+
LE => Simplification::Reducible(CmpOp::Le),
117+
GT => Simplification::Reducible(CmpOp::Gt),
118+
NEQ => Simplification::Reducible(CmpOp::Neq),
119+
GE => Simplification::Reducible(CmpOp::Ge),
120+
TRUE => Simplification::AlwaysTrue,
121+
_ => unreachable!(),
122+
}
123+
}
124+
125+
fn bool_op(sp!(_, bop_): &BinOp) -> Option<BoolOp> {
126+
Some(match bop_ {
127+
BinOp_::And => BoolOp::And,
128+
BinOp_::Or => BoolOp::Or,
129+
BinOp_::Eq
130+
| BinOp_::Lt
131+
| BinOp_::Gt
132+
| BinOp_::Le
133+
| BinOp_::Ge
134+
| BinOp_::Add
135+
| BinOp_::Sub
136+
| BinOp_::Mul
137+
| BinOp_::Mod
138+
| BinOp_::Div
139+
| BinOp_::BitOr
140+
| BinOp_::BitAnd
141+
| BinOp_::Xor
142+
| BinOp_::Shl
143+
| BinOp_::Shr
144+
| BinOp_::Range
145+
| BinOp_::Implies
146+
| BinOp_::Iff
147+
| BinOp_::Neq => return None,
148+
})
149+
}
150+
151+
fn cmp_op(sp!(_, bop_): &BinOp) -> Option<CmpOp> {
152+
Some(match bop_ {
153+
BinOp_::Eq => CmpOp::Eq,
154+
BinOp_::Neq => CmpOp::Neq,
155+
BinOp_::Lt => CmpOp::Lt,
156+
BinOp_::Gt => CmpOp::Gt,
157+
BinOp_::Le => CmpOp::Le,
158+
BinOp_::Ge => CmpOp::Ge,
159+
160+
BinOp_::Add
161+
| BinOp_::Sub
162+
| BinOp_::Mul
163+
| BinOp_::Mod
164+
| BinOp_::Div
165+
| BinOp_::BitOr
166+
| BinOp_::BitAnd
167+
| BinOp_::Xor
168+
| BinOp_::Shl
169+
| BinOp_::Shr
170+
| BinOp_::Range
171+
| BinOp_::Implies
172+
| BinOp_::Iff
173+
| BinOp_::And
174+
| BinOp_::Or => return None,
175+
})
176+
}
177+
178+
fn flip(op: CmpOp) -> CmpOp {
179+
match op {
180+
CmpOp::Eq => CmpOp::Eq,
181+
CmpOp::Neq => CmpOp::Neq,
182+
CmpOp::Lt => CmpOp::Gt,
183+
CmpOp::Gt => CmpOp::Lt,
184+
CmpOp::Le => CmpOp::Ge,
185+
CmpOp::Ge => CmpOp::Le,
186+
}
187+
}
188+
189+
fn binop_case(
190+
outer_bop: &BinOp,
191+
l1: &T::Exp,
192+
op_l: &BinOp,
193+
r1: &T::Exp,
194+
l2: &T::Exp,
195+
op_r: &BinOp,
196+
r2: &T::Exp,
197+
) -> Option<(BoolOp, CmpOp, CmpOp)> {
198+
let outer = bool_op(outer_bop)?;
199+
let inner_l = cmp_op(op_l)?;
200+
let inner_r = cmp_op(op_r)?;
201+
let (inner_l, inner_r) = operand_case(l1, inner_l, r1, l2, inner_r, r2)?;
202+
Some((outer, inner_l, inner_r))
203+
}
204+
205+
fn operand_case(
206+
l1: &T::Exp,
207+
op1: CmpOp,
208+
r1: &T::Exp,
209+
l2: &T::Exp,
210+
op2: CmpOp,
211+
r2: &T::Exp,
212+
) -> Option<(CmpOp, CmpOp)> {
213+
if same_value_exp(l1, l2) && same_value_exp(r1, r2) {
214+
Some((op1, op2))
215+
} else if same_value_exp(l1, r2) && same_value_exp(r1, l2) {
216+
Some((op1, flip(op2)))
217+
} else {
218+
None
219+
}
220+
}
221+
222+
impl std::fmt::Display for CmpOp {
223+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224+
write!(
225+
f,
226+
"{}",
227+
match self {
228+
CmpOp::Eq => "==",
229+
CmpOp::Neq => "!=",
230+
CmpOp::Lt => "<",
231+
CmpOp::Gt => ">",
232+
CmpOp::Le => "<=",
233+
CmpOp::Ge => ">=",
234+
}
235+
)
236+
}
237+
}

external-crates/move/crates/move-compiler/src/linters/mod.rs

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::{
1414
};
1515

1616
pub mod abort_constant;
17+
pub mod combinable_comparisons;
1718
pub mod constant_naming;
1819
pub mod equal_operands;
1920
pub mod loop_without_exit;
@@ -169,6 +170,12 @@ lints!(
169170
"always_equal_operands",
170171
"redundant, always-equal operands for binary operation"
171172
),
173+
(
174+
CombinableComparisons,
175+
LinterDiagnosticCategory::Complexity,
176+
"combinable_comparisons",
177+
"comparison operations condition can be simplified"
178+
)
172179
);
173180

174181
pub const ALLOW_ATTR_CATEGORY: &str = "lint";
@@ -207,6 +214,7 @@ pub fn linter_visitors(level: LintLevel) -> Vec<Visitor> {
207214
redundant_ref_deref::RedundantRefDeref.visitor(),
208215
unnecessary_unit::UnnecessaryUnit.visitor(),
209216
equal_operands::EqualOperands.visitor(),
217+
combinable_comparisons::CombinableComparisons.visitor(),
210218
]
211219
}
212220
}

0 commit comments

Comments
 (0)