Skip to content

optimize booleans #790

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

Merged
merged 24 commits into from
Mar 30, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
93d097e
better simplification
oli-obk Mar 23, 2016
57faa5a
improve bracket display
oli-obk Mar 23, 2016
1f1f09b
also compute minimal product of sum form
oli-obk Mar 23, 2016
25ed62f
improve lint attribute detail
oli-obk Mar 23, 2016
5911cca
merge multiple equal terminals into one
oli-obk Mar 23, 2016
050d7fd
fallout and tests
oli-obk Mar 23, 2016
0a78a79
bugfix in quine-mc_cluskey 0.2.1
oli-obk Mar 24, 2016
288ea79
treat macros as terminals to prevent `cfg!` from giving platform spec…
oli-obk Mar 24, 2016
03833f6
differentiate between logic bugs and optimizable expressions
oli-obk Mar 24, 2016
37cee84
negations around expressions can make things simpler
oli-obk Mar 24, 2016
76ab801
if a < b { ... } if a >= b { ... } what am I doing?
oli-obk Mar 24, 2016
e7013a3
update lints
oli-obk Mar 24, 2016
0f92f84
String::extend -> String::push_str
oli-obk Mar 24, 2016
dd6bee3
collect stats on bool ops and negations in an expression
oli-obk Mar 24, 2016
6904fd5
add tests showing the current level of minimization with ==
oli-obk Mar 24, 2016
25bbde0
a small refactoring for readability
oli-obk Mar 24, 2016
3a0791e
make sure `a < b` and `a >= b` are considered equal by SpanlessEq
oli-obk Mar 24, 2016
96be287
detect negations of terminals like a != b vs a == b
oli-obk Mar 24, 2016
be72883
more tests
oli-obk Mar 29, 2016
216edba
accidentally forgot about improvements if there were multiplie candid…
oli-obk Mar 29, 2016
b05dd13
added brackets and fixed compiler comments
oli-obk Mar 29, 2016
e9c87c7
`!(a == b)` --> `a != b`
oli-obk Mar 29, 2016
fa48ee6
dogfood
oli-obk Mar 29, 2016
2917484
make `nonminimal_bool` allow-by-default
oli-obk Mar 30, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ regex_macros = { version = "0.1.33", optional = true }
semver = "0.2.1"
toml = "0.1"
unicode-normalization = "0.1"
quine-mc_cluskey = "0.2.2"

[dev-dependencies]
compiletest_rs = "0.1.0"
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Table of contents:
* [License](#license)

##Lints
There are 137 lints included in this crate:
There are 139 lints included in this crate:

name | default | meaning
---------------------------------------------------------------------------------------------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -75,6 +75,7 @@ name
[let_and_return](https://github.com/Manishearth/rust-clippy/wiki#let_and_return) | warn | creating a let-binding and then immediately returning it like `let x = expr; x` at the end of a block
[let_unit_value](https://github.com/Manishearth/rust-clippy/wiki#let_unit_value) | warn | creating a let binding to a value of unit type, which usually can't be used afterwards
[linkedlist](https://github.com/Manishearth/rust-clippy/wiki#linkedlist) | warn | usage of LinkedList, usually a vector is faster, or a more specialized data structure like a VecDeque
[logic_bug](https://github.com/Manishearth/rust-clippy/wiki#logic_bug) | warn | checks for boolean expressions that contain terminals which can be eliminated
[manual_swap](https://github.com/Manishearth/rust-clippy/wiki#manual_swap) | warn | manual swap
[many_single_char_names](https://github.com/Manishearth/rust-clippy/wiki#many_single_char_names) | warn | too many single character bindings
[map_clone](https://github.com/Manishearth/rust-clippy/wiki#map_clone) | warn | using `.map(|x| x.clone())` to clone an iterator or option's contents (recommends `.cloned()` instead)
Expand All @@ -97,6 +98,7 @@ name
[new_without_default](https://github.com/Manishearth/rust-clippy/wiki#new_without_default) | warn | `fn new() -> Self` method without `Default` implementation
[no_effect](https://github.com/Manishearth/rust-clippy/wiki#no_effect) | warn | statements with no effect
[non_ascii_literal](https://github.com/Manishearth/rust-clippy/wiki#non_ascii_literal) | allow | using any literal non-ASCII chars in a string literal; suggests using the \\u escape instead
[nonminimal_bool](https://github.com/Manishearth/rust-clippy/wiki#nonminimal_bool) | allow | checks for boolean expressions that can be written more concisely
[nonsensical_open_options](https://github.com/Manishearth/rust-clippy/wiki#nonsensical_open_options) | warn | nonsensical combination of options for opening a file
[ok_expect](https://github.com/Manishearth/rust-clippy/wiki#ok_expect) | warn | using `ok().expect()`, which gives worse error messages than calling `expect` directly on the Result
[option_map_unwrap_or](https://github.com/Manishearth/rust-clippy/wiki#option_map_unwrap_or) | warn | using `Option.map(f).unwrap_or(a)`, which is more succinctly expressed as `map_or(a, f)`
Expand Down
362 changes: 362 additions & 0 deletions src/booleans.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,362 @@
use rustc::lint::{LintArray, LateLintPass, LateContext, LintPass};
use rustc_front::hir::*;
use rustc_front::intravisit::*;
use syntax::ast::{LitKind, DUMMY_NODE_ID};
use syntax::codemap::{DUMMY_SP, dummy_spanned};
use utils::{span_lint_and_then, in_macro, snippet_opt, SpanlessEq};

/// **What it does:** This lint checks for boolean expressions that can be written more concisely
///
/// **Why is this bad?** Readability of boolean expressions suffers from unnecesessary duplication
///
/// **Known problems:** Ignores short circuting behavior of `||` and `&&`. Ignores `|`, `&` and `^`.
///
/// **Example:** `if a && true` should be `if a` and `!(a == b)` should be `a != b`
declare_lint! {
pub NONMINIMAL_BOOL, Allow,
"checks for boolean expressions that can be written more concisely"
}

/// **What it does:** This lint checks for boolean expressions that contain terminals that can be eliminated
///
/// **Why is this bad?** This is most likely a logic bug
///
/// **Known problems:** Ignores short circuiting behavior
///
/// **Example:** The `b` in `if a && b || a` is unnecessary because the expression is equivalent to `if a`
declare_lint! {
pub LOGIC_BUG, Warn,
"checks for boolean expressions that contain terminals which can be eliminated"
}

#[derive(Copy,Clone)]
pub struct NonminimalBool;

impl LintPass for NonminimalBool {
fn get_lints(&self) -> LintArray {
lint_array!(NONMINIMAL_BOOL, LOGIC_BUG)
}
}

impl LateLintPass for NonminimalBool {
fn check_item(&mut self, cx: &LateContext, item: &Item) {
NonminimalBoolVisitor(cx).visit_item(item)
}
}

struct NonminimalBoolVisitor<'a, 'tcx: 'a>(&'a LateContext<'a, 'tcx>);

use quine_mc_cluskey::Bool;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that use here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to annoy enough so someone writes a lint warning about uses in odd places?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems legit 😄

struct Hir2Qmm<'a, 'tcx: 'a, 'v> {
terminals: Vec<&'v Expr>,
cx: &'a LateContext<'a, 'tcx>
}

impl<'a, 'tcx, 'v> Hir2Qmm<'a, 'tcx, 'v> {
fn extract(&mut self, op: BinOp_, a: &[&'v Expr], mut v: Vec<Bool>) -> Result<Vec<Bool>, String> {
for a in a {
if let ExprBinary(binop, ref lhs, ref rhs) = a.node {
if binop.node == op {
v = self.extract(op, &[lhs, rhs], v)?;
continue;
}
}
v.push(self.run(a)?);
}
Ok(v)
}

fn run(&mut self, e: &'v Expr) -> Result<Bool, String> {
// prevent folding of `cfg!` macros and the like
if !in_macro(self.cx, e.span) {
match e.node {
ExprUnary(UnNot, ref inner) => return Ok(Bool::Not(box self.run(inner)?)),
ExprBinary(binop, ref lhs, ref rhs) => {
match binop.node {
BiOr => return Ok(Bool::Or(self.extract(BiOr, &[lhs, rhs], Vec::new())?)),
BiAnd => return Ok(Bool::And(self.extract(BiAnd, &[lhs, rhs], Vec::new())?)),
_ => {},
}
},
ExprLit(ref lit) => {
match lit.node {
LitKind::Bool(true) => return Ok(Bool::True),
LitKind::Bool(false) => return Ok(Bool::False),
_ => {},
}
},
_ => {},
}
}
for (n, expr) in self.terminals.iter().enumerate() {
if SpanlessEq::new(self.cx).ignore_fn().eq_expr(e, expr) {
#[allow(cast_possible_truncation)]
return Ok(Bool::Term(n as u8));
}
let negated = match e.node {
ExprBinary(binop, ref lhs, ref rhs) => {
let mk_expr = |op| Expr {
id: DUMMY_NODE_ID,
span: DUMMY_SP,
attrs: None,
node: ExprBinary(dummy_spanned(op), lhs.clone(), rhs.clone()),
};
match binop.node {
BiEq => mk_expr(BiNe),
BiNe => mk_expr(BiEq),
BiGt => mk_expr(BiLe),
BiGe => mk_expr(BiLt),
BiLt => mk_expr(BiGe),
BiLe => mk_expr(BiGt),
_ => continue,
}
},
_ => continue,
};
if SpanlessEq::new(self.cx).ignore_fn().eq_expr(&negated, expr) {
#[allow(cast_possible_truncation)]
return Ok(Bool::Not(Box::new(Bool::Term(n as u8))));
}
}
let n = self.terminals.len();
self.terminals.push(e);
if n < 32 {
#[allow(cast_possible_truncation)]
Ok(Bool::Term(n as u8))
} else {
Err("too many literals".to_owned())
}
}
}

fn suggest(cx: &LateContext, suggestion: &Bool, terminals: &[&Expr]) -> String {
fn recurse(brackets: bool, cx: &LateContext, suggestion: &Bool, terminals: &[&Expr], mut s: String) -> String {
use quine_mc_cluskey::Bool::*;
let snip = |e: &Expr| snippet_opt(cx, e.span).expect("don't try to improve booleans created by macros");
match *suggestion {
True => {
s.push_str("true");
s
},
False => {
s.push_str("false");
s
},
Not(ref inner) => {
match **inner {
And(_) | Or(_) => {
s.push('!');
recurse(true, cx, inner, terminals, s)
},
Term(n) => {
if let ExprBinary(binop, ref lhs, ref rhs) = terminals[n as usize].node {
let op = match binop.node {
BiEq => " != ",
BiNe => " == ",
BiLt => " >= ",
BiGt => " <= ",
BiLe => " > ",
BiGe => " < ",
_ => {
s.push('!');
return recurse(true, cx, inner, terminals, s)
},
};
s.push_str(&snip(lhs));
s.push_str(op);
s.push_str(&snip(rhs));
s
} else {
s.push('!');
recurse(false, cx, inner, terminals, s)
}
},
_ => {
s.push('!');
recurse(false, cx, inner, terminals, s)
},
}
},
And(ref v) => {
if brackets {
s.push('(');
}
if let Or(_) = v[0] {
s = recurse(true, cx, &v[0], terminals, s);
} else {
s = recurse(false, cx, &v[0], terminals, s);
}
for inner in &v[1..] {
s.push_str(" && ");
if let Or(_) = *inner {
s = recurse(true, cx, inner, terminals, s);
} else {
s = recurse(false, cx, inner, terminals, s);
}
}
if brackets {
s.push(')');
}
s
},
Or(ref v) => {
if brackets {
s.push('(');
}
s = recurse(false, cx, &v[0], terminals, s);
for inner in &v[1..] {
s.push_str(" || ");
s = recurse(false, cx, inner, terminals, s);
}
if brackets {
s.push(')');
}
s
},
Term(n) => {
if brackets {
if let ExprBinary(..) = terminals[n as usize].node {
s.push('(');
}
}
s.push_str(&snip(&terminals[n as usize]));
if brackets {
if let ExprBinary(..) = terminals[n as usize].node {
s.push(')');
}
}
s
}
}
}
recurse(false, cx, suggestion, terminals, String::new())
}

fn simple_negate(b: Bool) -> Bool {
use quine_mc_cluskey::Bool::*;
match b {
True => False,
False => True,
t @ Term(_) => Not(Box::new(t)),
And(mut v) => {
for el in &mut v {
*el = simple_negate(::std::mem::replace(el, True));
}
Or(v)
},
Or(mut v) => {
for el in &mut v {
*el = simple_negate(::std::mem::replace(el, True));
}
And(v)
},
Not(inner) => *inner,
}
}

#[derive(Default)]
struct Stats {
terminals: [usize; 32],
negations: usize,
ops: usize,
}

fn terminal_stats(b: &Bool) -> Stats {
fn recurse(b: &Bool, stats: &mut Stats) {
match *b {
True | False => stats.ops += 1,
Not(ref inner) => {
match **inner {
And(_) | Or(_) => stats.ops += 1, // brackets are also operations
_ => stats.negations += 1,
}
recurse(inner, stats);
},
And(ref v) | Or(ref v) => {
stats.ops += v.len() - 1;
for inner in v {
recurse(inner, stats);
}
},
Term(n) => stats.terminals[n as usize] += 1,
}
}
use quine_mc_cluskey::Bool::*;
let mut stats = Stats::default();
recurse(b, &mut stats);
stats
}

impl<'a, 'tcx> NonminimalBoolVisitor<'a, 'tcx> {
fn bool_expr(&self, e: &Expr) {
let mut h2q = Hir2Qmm {
terminals: Vec::new(),
cx: self.0,
};
if let Ok(expr) = h2q.run(e) {
let stats = terminal_stats(&expr);
let mut simplified = expr.simplify();
for simple in Bool::Not(Box::new(expr.clone())).simplify() {
match simple {
Bool::Not(_) | Bool::True | Bool::False => {},
_ => simplified.push(Bool::Not(Box::new(simple.clone()))),
}
let simple_negated = simple_negate(simple);
if simplified.iter().any(|s| *s == simple_negated) {
continue;
}
simplified.push(simple_negated);
}
let mut improvements = Vec::new();
'simplified: for suggestion in &simplified {
let simplified_stats = terminal_stats(&suggestion);
let mut improvement = false;
for i in 0..32 {
// ignore any "simplifications" that end up requiring a terminal more often than in the original expression
if stats.terminals[i] < simplified_stats.terminals[i] {
continue 'simplified;
}
if stats.terminals[i] != 0 && simplified_stats.terminals[i] == 0 {
span_lint_and_then(self.0, LOGIC_BUG, e.span, "this boolean expression contains a logic bug", |db| {
db.span_help(h2q.terminals[i].span, "this expression can be optimized out by applying boolean operations to the outer expression");
db.span_suggestion(e.span, "it would look like the following", suggest(self.0, suggestion, &h2q.terminals));
});
// don't also lint `NONMINIMAL_BOOL`
return;
}
// if the number of occurrences of a terminal decreases or any of the stats decreases while none increases
improvement |= (stats.terminals[i] > simplified_stats.terminals[i]) ||
(stats.negations > simplified_stats.negations && stats.ops == simplified_stats.ops) ||
(stats.ops > simplified_stats.ops && stats.negations == simplified_stats.negations);
}
if improvement {
improvements.push(suggestion);
}
}
if !improvements.is_empty() {
span_lint_and_then(self.0, NONMINIMAL_BOOL, e.span, "this boolean expression can be simplified", |db| {
for suggestion in &improvements {
db.span_suggestion(e.span, "try", suggest(self.0, suggestion, &h2q.terminals));
}
});
}
}
}
}

impl<'a, 'v, 'tcx> Visitor<'v> for NonminimalBoolVisitor<'a, 'tcx> {
fn visit_expr(&mut self, e: &'v Expr) {
if in_macro(self.0, e.span) { return }
match e.node {
ExprBinary(binop, _, _) if binop.node == BiOr || binop.node == BiAnd => self.bool_expr(e),
ExprUnary(UnNot, ref inner) => {
if self.0.tcx.node_types()[&inner.id].is_bool() {
self.bool_expr(e);
} else {
walk_expr(self, e);
}
},
_ => walk_expr(self, e),
}
}
}
Loading