• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

basilisp-lang / basilisp / 10798992192

10 Sep 2024 07:03PM CUT coverage: 98.807% (-0.09%) from 98.892%
10798992192

Pull #1044

github

web-flow
Merge bc24d2bbb into ae617c7b3
Pull Request #1044: test runner for basilisp.test #980

1286 of 1294 branches covered (99.38%)

Branch coverage included in aggregate %.

8570 of 8681 relevant lines covered (98.72%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.09
/src/basilisp/lang/compiler/optimizer.py
1
import ast
1✔
2
import functools
1✔
3
from collections import deque
1✔
4
from contextlib import contextmanager
1✔
5
from typing import Deque, Iterable, List, Optional, Set
1✔
6

7
from basilisp.lang.compiler.utils import ast_FunctionDef, ast_index
1✔
8

9

10
def _filter_dead_code(nodes: Iterable[ast.stmt]) -> List[ast.stmt]:
1✔
11
    """Return a list of body nodes, trimming out unreachable code (any
12
    statements appearing after `break`, `continue`, `raise`, and `return`
13
    nodes)."""
14
    new_nodes: List[ast.stmt] = []
1✔
15
    for node in nodes:
1✔
16
        if isinstance(node, (ast.Break, ast.Continue, ast.Raise, ast.Return)):
1✔
17
            new_nodes.append(node)
1✔
18
            break
1✔
19
        new_nodes.append(node)
1✔
20
    return new_nodes
1✔
21

22

23
def _needs_eq_operator(arg: ast.expr) -> bool:
1✔
24
    return isinstance(arg, ast.Constant) and all(
1✔
25
        arg.value is not v for v in (True, False, None, ...)
26
    )
27

28

29
@functools.singledispatch
1✔
30
def _optimize_operator_call(  # pylint: disable=unused-argument
1✔
31
    fn: ast.AST, node: ast.Call
32
) -> ast.AST:
33
    return node
1✔
34

35

36
@_optimize_operator_call.register(ast.Attribute)
1✔
37
def _optimize_operator_call_attr(  # pylint: disable=too-many-return-statements
1✔
38
    fn: ast.Attribute, node: ast.Call
39
) -> ast.AST:
40
    """Optimize calls to the Python `operator` module down to use the raw Python
41
    operators.
42

43
    Using Python operators directly will allow for more direct bytecode to be
44
    emitted by the Python compiler and take advantage of any additional performance
45
    improvements in future versions of Python."""
46
    if isinstance(fn.value, ast.Name) and fn.value.id == "operator":
1✔
47
        binop = {
1✔
48
            "add": ast.Add,
49
            "and_": ast.BitAnd,
50
            "floordiv": ast.FloorDiv,
51
            "lshift": ast.LShift,
52
            "mod": ast.Mod,
53
            "mul": ast.Mult,
54
            "matmul": ast.MatMult,
55
            "or_": ast.BitOr,
56
            "pow": ast.Pow,
57
            "rshift": ast.RShift,
58
            "sub": ast.Sub,
59
            "truediv": ast.Div,
60
            "xor": ast.BitXor,
61
        }.get(fn.attr)
62
        if binop is not None:
1✔
63
            arg1, arg2 = node.args
1✔
64
            assert len(node.args) == 2
1✔
65
            return ast.BinOp(arg1, binop(), arg2)
1✔
66

67
        unaryop = {"not_": ast.Not, "inv": ast.Invert, "invert": ast.Invert}.get(
1✔
68
            fn.attr
69
        )
70
        if unaryop is not None:
1✔
71
            arg = node.args[0]
1✔
72
            assert len(node.args) == 1
1✔
73
            return ast.UnaryOp(unaryop(), arg)
1✔
74

75
        compareop = {
1✔
76
            "lt": ast.Lt,
77
            "le": ast.LtE,
78
            "eq": ast.Eq,
79
            "ne": ast.NotEq,
80
            "gt": ast.Gt,
81
            "ge": ast.GtE,
82
        }.get(fn.attr)
83
        if compareop is not None:
1✔
84
            arg1, arg2 = node.args
1✔
85
            assert len(node.args) == 2
1✔
86
            return ast.Compare(arg1, [compareop()], [arg2])
1✔
87

88
        isop = {
1✔
89
            "is_": (ast.Is, ast.Eq),
90
            "is_not": (ast.IsNot, ast.NotEq),
91
        }.get(fn.attr)
92
        if isop is not None:
1✔
93
            isoper, eqoper = isop
1✔
94
            arg1, arg2 = node.args
1✔
95
            assert len(node.args) == 2
1✔
96
            oper = (
1✔
97
                eqoper if any(_needs_eq_operator(arg) for arg in node.args) else isoper
98
            )
99
            return ast.Compare(arg1, [oper()], [arg2])
1✔
100

101
        if fn.attr == "contains":
1✔
102
            arg1, arg2 = node.args
1✔
103
            assert len(node.args) == 2
1✔
104
            return ast.Compare(arg2, [ast.In()], [arg1])
1✔
105

106
        if fn.attr == "delitem":
1✔
107
            target, index = node.args
×
108
            assert len(node.args) == 2
×
109
            return ast.Delete(
×
110
                targets=[
111
                    ast.Subscript(value=target, slice=ast_index(index), ctx=ast.Del())
112
                ]
113
            )
114

115
        if fn.attr == "getitem":
1✔
116
            target, index = node.args
1✔
117
            assert len(node.args) == 2
1✔
118
            return ast.Subscript(value=target, slice=ast_index(index), ctx=ast.Load())
1✔
119

120
    return node
1✔
121

122

123
class PythonASTOptimizer(ast.NodeTransformer):
1✔
124
    __slots__ = ("_global_ctx",)
1✔
125

126
    def __init__(self):
1✔
127
        self._global_ctx: Deque[Set[str]] = deque([set()])
1✔
128

129
    @contextmanager
1✔
130
    def _new_global_context(self):
1✔
131
        """Context manager which sets a new Python `global` context."""
132
        self._global_ctx.append(set())
1✔
133
        try:
1✔
134
            yield
1✔
135
        finally:
136
            self._global_ctx.pop()
1✔
137

138
    @property
1✔
139
    def _global_context(self) -> Set[str]:
1✔
140
        """Return the current Python `global` context."""
141
        return self._global_ctx[-1]
1✔
142

143
    def visit_Call(self, node: ast.Call) -> ast.AST:
1✔
144
        """Eliminate most calls to Python's `operator` module in favor of using native
145
        operators."""
146
        new_node = self.generic_visit(node)
1✔
147
        if isinstance(new_node, ast.Call):
1✔
148
            return ast.copy_location(
1✔
149
                _optimize_operator_call(node.func, new_node), new_node
150
            )
151
        return new_node
×
152

153
    def visit_ExceptHandler(self, node: ast.ExceptHandler) -> Optional[ast.AST]:
1✔
154
        """Eliminate dead code from except handler bodies."""
155
        new_node = self.generic_visit(node)
1✔
156
        assert isinstance(new_node, ast.ExceptHandler)
1✔
157
        return ast.copy_location(
1✔
158
            ast.ExceptHandler(
159
                type=new_node.type,
160
                name=new_node.name,
161
                body=_filter_dead_code(new_node.body),
162
            ),
163
            new_node,
164
        )
165

166
    def visit_Expr(self, node: ast.Expr) -> Optional[ast.Expr]:
1✔
167
        """Eliminate no-op constant expressions which are in the tree
168
        as standalone statements."""
169
        if isinstance(node.value, (ast.Constant, ast.Name)):
1✔
170
            return None
1✔
171
        return node
1✔
172

173
    def visit_FunctionDef(self, node: ast.FunctionDef) -> Optional[ast.AST]:
1✔
174
        """Eliminate dead code from function bodies."""
175
        with self._new_global_context():
1✔
176
            new_node = self.generic_visit(node)
1✔
177
        assert isinstance(new_node, ast.FunctionDef)
1✔
178
        return ast.copy_location(
1✔
179
            ast_FunctionDef(
180
                name=new_node.name,
181
                args=new_node.args,
182
                body=_filter_dead_code(new_node.body),
183
                decorator_list=new_node.decorator_list,
184
                returns=new_node.returns,
185
            ),
186
            new_node,
187
        )
188

189
    def visit_Global(self, node: ast.Global) -> Optional[ast.Global]:
1✔
190
        """Eliminate redundant name declarations inside a Python `global` statement.
191

192
        Python `global` statements may only refer to a name prior to its declaration.
193
        Global contexts track names in prior `global` declarations and eliminate
194
        redundant names in `global` declarations. If all of the names in the current
195
        `global` statement are redundant, the entire node will be omitted."""
196
        new_names = set(node.names) - self._global_context
1✔
197
        self._global_context.update(new_names)
1✔
198
        return (
1✔
199
            ast.copy_location(ast.Global(names=list(new_names)), node)
200
            if new_names
201
            else None
202
        )
203

204
    def visit_If(self, node: ast.If) -> Optional[ast.AST]:
1✔
205
        """Eliminate dead code from if/elif bodies.
206

207
        If the new `if` statement `body` is empty after eliminating dead code, replace
208
        the body with the `orelse` body and negate the `if` condition.
209

210
        If both the `body` and `orelse` body are empty, eliminate the node from the
211
        tree."""
212
        new_node = self.generic_visit(node)
1✔
213
        assert isinstance(new_node, ast.If)
1✔
214

215
        new_body = _filter_dead_code(new_node.body)
1✔
216
        new_orelse = _filter_dead_code(new_node.orelse)
1✔
217

218
        if new_body:
1✔
219
            ifstmt = ast.If(
1✔
220
                test=new_node.test,
221
                body=new_body,
222
                orelse=new_orelse,
223
            )
224
        elif new_orelse:
1✔
225
            ifstmt = ast.If(
1✔
226
                test=ast.UnaryOp(op=ast.Not(), operand=new_node.test),
227
                body=new_orelse,
228
                orelse=[],
229
            )
230
        else:
231
            return None
×
232

233
        return ast.copy_location(ifstmt, new_node)
1✔
234

235
    def visit_While(self, node: ast.While) -> Optional[ast.AST]:
1✔
236
        """Eliminate dead code from while bodies."""
237
        new_node = self.generic_visit(node)
1✔
238
        assert isinstance(new_node, ast.While)
1✔
239
        return ast.copy_location(
1✔
240
            ast.While(
241
                test=new_node.test,
242
                body=_filter_dead_code(new_node.body),
243
                orelse=_filter_dead_code(new_node.orelse),
244
            ),
245
            new_node,
246
        )
247

248
    def visit_Try(self, node: ast.Try) -> Optional[ast.AST]:
1✔
249
        """Eliminate dead code from except try bodies."""
250
        new_node = self.generic_visit(node)
1✔
251
        assert isinstance(new_node, ast.Try)
1✔
252
        return ast.copy_location(
1✔
253
            ast.Try(
254
                body=_filter_dead_code(new_node.body),
255
                handlers=new_node.handlers,
256
                orelse=_filter_dead_code(new_node.orelse),
257
                finalbody=_filter_dead_code(new_node.finalbody),
258
            ),
259
            new_node,
260
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc