about summary refs log tree commit diff
path: root/tvix/eval/src/compiler/optimiser.rs
blob: ace5335d6835c2ba11a27b822263fc2125a6c492 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! Helper functions for extending the compiler with more linter-like
//! functionality while compiling (i.e. smarter warnings).

use super::*;

use ast::Expr;

/// Optimise the given expression where possible.
pub(super) fn optimise_expr(c: &mut Compiler, slot: LocalIdx, expr: ast::Expr) -> ast::Expr {
    match expr {
        Expr::BinOp(_) => optimise_bin_op(c, slot, expr),
        Expr::Paren(_) => optimise_paren(c, expr),
        _ => expr.to_owned(),
    }
}

enum LitBool {
    Expr(Expr),
    True(Expr),
    False(Expr),
}

/// Is this a literal boolean, or something else?
fn is_lit_bool(expr: ast::Expr) -> LitBool {
    if let ast::Expr::Ident(ident) = &expr {
        match ident.ident_token().unwrap().text() {
            "true" => LitBool::True(expr),
            "false" => LitBool::False(expr),
            _ => LitBool::Expr(expr),
        }
    } else {
        LitBool::Expr(expr)
    }
}

/// Detect useless binary operations (i.e. useless bool comparisons).
fn optimise_bin_op(c: &mut Compiler, slot: LocalIdx, expr: ast::Expr) -> ast::Expr {
    use ast::BinOpKind;

    // bail out of this check if the user has overridden either `true`
    // or `false` identifiers. Note that they will have received a
    // separate warning about this for shadowing the global(s).
    if c.is_user_defined("true") || c.is_user_defined("false") {
        return expr;
    }

    if let Expr::BinOp(op) = &expr {
        let lhs = is_lit_bool(op.lhs().unwrap());
        let rhs = is_lit_bool(op.rhs().unwrap());

        match (op.operator().unwrap(), lhs, rhs) {
            // useless `false` arm in `||` expression
            (BinOpKind::Or, LitBool::False(f), LitBool::Expr(other))
            | (BinOpKind::Or, LitBool::Expr(other), LitBool::False(f)) => {
                c.emit_warning(
                    &f,
                    WarningKind::UselessBoolOperation(
                        "this `false` has no effect on the result of the comparison",
                    ),
                );

                return other;
            }

            // useless `true` arm in `&&` expression
            (BinOpKind::And, LitBool::True(t), LitBool::Expr(other))
            | (BinOpKind::And, LitBool::Expr(other), LitBool::True(t)) => {
                c.emit_warning(
                    &t,
                    WarningKind::UselessBoolOperation(
                        "this `true` has no effect on the result of the comparison",
                    ),
                );

                return other;
            }

            // useless `||` expression (one arm is `true`), return
            // `true` directly (and warn about dead code on the right)
            (BinOpKind::Or, LitBool::True(t), LitBool::Expr(other)) => {
                c.emit_warning(
                    op,
                    WarningKind::UselessBoolOperation("this expression is always true"),
                );

                c.compile_dead_code(slot, other);

                return t;
            }

            (BinOpKind::Or, _, LitBool::True(t)) | (BinOpKind::Or, LitBool::True(t), _) => {
                c.emit_warning(
                    op,
                    WarningKind::UselessBoolOperation("this expression is always true"),
                );

                return t;
            }

            // useless `&&` expression (one arm is `false), same as above
            (BinOpKind::And, LitBool::False(f), LitBool::Expr(other)) => {
                c.emit_warning(
                    op,
                    WarningKind::UselessBoolOperation("this expression is always false"),
                );

                c.compile_dead_code(slot, other);

                return f;
            }

            (BinOpKind::And, _, LitBool::False(f)) | (BinOpKind::Or, LitBool::False(f), _) => {
                c.emit_warning(
                    op,
                    WarningKind::UselessBoolOperation("this expression is always false"),
                );

                return f;
            }

            _ => { /* nothing to optimise */ }
        }
    }

    expr
}

/// Detect useless parenthesis around primitive expressions.
fn optimise_paren(c: &mut Compiler, expr: ast::Expr) -> ast::Expr {
    if let Expr::Paren(inner) = &expr {
        let inner = inner.expr().unwrap();

        if let Expr::Paren(_) = &inner {
            c.emit_warning(&expr, WarningKind::UselessParens);
            return optimise_paren(c, inner);
        }

        if let Expr::Literal(_)
        | Expr::Str(_)
        | Expr::Select(_)
        | Expr::List(_)
        | Expr::AttrSet(_)
        | Expr::Ident(_) = &inner
        {
            c.emit_warning(&expr, WarningKind::UselessParens);
            return inner;
        }
    }

    expr
}