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

basilisp-lang / basilisp / 7379247703

01 Jan 2024 06:37PM CUT coverage: 99.008%. Remained the same
7379247703

push

github

web-flow
callable var (#768)

Hi,

could you please review compatibility patch with Clojure to make vars
callable. It addresses #767.

I am not sure this if the `class Var` is the right place to make vars
callable or the analyzer, which should expand them to a callable var
value last node.

Nevertheless, I will kindly request your help with the type hinting,
currently it will fail the linter with the following error

```
src/basilisp/lang/runtime.py:279:15: E1102: self.value is not callable (not-callable)
```

but not sure how to fix it, I've tried the class Var `value` property
method to return a maybe Callable but it didn't work.

Thanks

Co-authored-by: ikappaki <ikappaki@users.noreply.github.com>

1691 of 1693 branches covered (0.0%)

Branch coverage included in aggregate %.

7892 of 7986 relevant lines covered (98.82%)

0.99 hits per line

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

95.8
/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

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

20

21
@functools.singledispatch
1✔
22
def _optimize_operator_call(  # pylint: disable=unused-argument
1✔
23
    fn: ast.AST, node: ast.Call
24
) -> ast.AST:
25
    return node
1✔
26

27

28
@_optimize_operator_call.register(ast.Attribute)
1✔
29
def _optimize_operator_call_attr(  # pylint: disable=too-many-return-statements
1✔
30
    fn: ast.Attribute, node: ast.Call
31
) -> ast.AST:
32
    """Optimize calls to the Python `operator` module down to use the raw Python
33
    operators.
34

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

59
        unaryop = {"not_": ast.Not, "inv": ast.Invert, "invert": ast.Invert}.get(
1✔
60
            fn.attr
61
        )
62
        if unaryop is not None:
1✔
63
            arg = node.args[0]
1✔
64
            assert len(node.args) == 1
1✔
65
            return ast.UnaryOp(unaryop(), arg)
1✔
66

67
        compareop = {
1✔
68
            "lt": ast.Lt,
69
            "le": ast.LtE,
70
            "eq": ast.Eq,
71
            "ne": ast.NotEq,
72
            "gt": ast.Gt,
73
            "ge": ast.GtE,
74
            "is_": ast.Is,
75
            "is_not": ast.IsNot,
76
        }.get(fn.attr)
77
        if compareop is not None:
1✔
78
            arg1, arg2 = node.args
1✔
79
            assert len(node.args) == 2
1✔
80
            return ast.Compare(arg1, [compareop()], [arg2])
1✔
81

82
        if fn.attr == "contains":
1✔
83
            arg1, arg2 = node.args
1✔
84
            assert len(node.args) == 2
1✔
85
            return ast.Compare(arg2, [ast.In()], [arg1])
1✔
86

87
        if fn.attr == "delitem":
1✔
88
            target, index = node.args
×
89
            assert len(node.args) == 2
×
90
            return ast.Delete(
×
91
                targets=[
92
                    ast.Subscript(
93
                        value=target, slice=ast.Index(value=index), ctx=ast.Del()
94
                    )
95
                ]
96
            )
97

98
        if fn.attr == "getitem":
1✔
99
            target, index = node.args
1✔
100
            assert len(node.args) == 2
1✔
101
            return ast.Subscript(
1✔
102
                value=target, slice=ast.Index(value=index), ctx=ast.Load()
103
            )
104

105
    return node
1✔
106

107

108
class PythonASTOptimizer(ast.NodeTransformer):
1✔
109
    __slots__ = ("_global_ctx",)
1✔
110

111
    def __init__(self):
1✔
112
        self._global_ctx: Deque[Set[str]] = deque([set()])
1✔
113

114
    @contextmanager
1✔
115
    def _new_global_context(self):
1✔
116
        """Context manager which sets a new Python `global` context."""
117
        self._global_ctx.append(set())
1✔
118
        try:
1✔
119
            yield
1✔
120
        finally:
121
            self._global_ctx.pop()
1✔
122

123
    @property
1✔
124
    def _global_context(self) -> Set[str]:
1✔
125
        """Return the current Python `global` context."""
126
        return self._global_ctx[-1]
1✔
127

128
    def visit_Call(self, node: ast.Call) -> ast.AST:
1✔
129
        """Eliminate most calls to Python's `operator` module in favor of using native
130
        operators."""
131
        new_node = self.generic_visit(node)
1✔
132
        if isinstance(new_node, ast.Call):
1✔
133
            return ast.copy_location(
1✔
134
                _optimize_operator_call(node.func, new_node), new_node
135
            )
136
        return new_node
×
137

138
    def visit_ExceptHandler(self, node: ast.ExceptHandler) -> Optional[ast.AST]:
1✔
139
        """Eliminate dead code from except handler bodies."""
140
        new_node = self.generic_visit(node)
1✔
141
        assert isinstance(new_node, ast.ExceptHandler)
1✔
142
        return ast.copy_location(
1✔
143
            ast.ExceptHandler(
144
                type=new_node.type,
145
                name=new_node.name,
146
                body=_filter_dead_code(new_node.body),
147
            ),
148
            new_node,
149
        )
150

151
    def visit_Expr(self, node: ast.Expr) -> Optional[ast.Expr]:
1✔
152
        """Eliminate no-op constant expressions which are in the tree
153
        as standalone statements."""
154
        if isinstance(node.value, (ast.Constant, ast.Name)):
1✔
155
            return None
1✔
156
        return node
1✔
157

158
    def visit_FunctionDef(self, node: ast.FunctionDef) -> Optional[ast.AST]:
1✔
159
        """Eliminate dead code from function bodies."""
160
        with self._new_global_context():
1✔
161
            new_node = self.generic_visit(node)
1✔
162
        assert isinstance(new_node, ast.FunctionDef)
1✔
163
        return ast.copy_location(
1✔
164
            ast.FunctionDef(
165
                name=new_node.name,
166
                args=new_node.args,
167
                body=_filter_dead_code(new_node.body),
168
                decorator_list=new_node.decorator_list,
169
                returns=new_node.returns,
170
            ),
171
            new_node,
172
        )
173

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

177
        Python `global` statements may only refer to a name prior to its declaration.
178
        Global contexts track names in prior `global` declarations and eliminate
179
        redundant names in `global` declarations. If all of the names in the current
180
        `global` statement are redundant, the entire node will be omitted."""
181
        new_names = set(node.names) - self._global_context
1✔
182
        self._global_context.update(new_names)
1✔
183
        return (
1✔
184
            ast.copy_location(ast.Global(names=list(new_names)), node)
185
            if new_names
186
            else None
187
        )
188

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

192
        If the new `if` statement `body` is empty after eliminating dead code, replace
193
        the body with the `orelse` body and negate the `if` condition.
194

195
        If both the `body` and `orelse` body are empty, eliminate the node from the
196
        tree."""
197
        new_node = self.generic_visit(node)
1✔
198
        assert isinstance(new_node, ast.If)
1✔
199

200
        new_body = _filter_dead_code(new_node.body)
1✔
201
        new_orelse = _filter_dead_code(new_node.orelse)
1✔
202

203
        if new_body:
1✔
204
            ifstmt = ast.If(
1✔
205
                test=new_node.test,
206
                body=new_body,
207
                orelse=new_orelse,
208
            )
209
        elif new_orelse:
1✔
210
            ifstmt = ast.If(
1✔
211
                test=ast.UnaryOp(op=ast.Not(), operand=new_node.test),
212
                body=new_orelse,
213
                orelse=[],
214
            )
215
        else:
216
            return None
×
217

218
        return ast.copy_location(ifstmt, new_node)
1✔
219

220
    def visit_While(self, node: ast.While) -> Optional[ast.AST]:
1✔
221
        """Eliminate dead code from while bodies."""
222
        new_node = self.generic_visit(node)
1✔
223
        assert isinstance(new_node, ast.While)
1✔
224
        return ast.copy_location(
1✔
225
            ast.While(
226
                test=new_node.test,
227
                body=_filter_dead_code(new_node.body),
228
                orelse=_filter_dead_code(new_node.orelse),
229
            ),
230
            new_node,
231
        )
232

233
    def visit_Try(self, node: ast.Try) -> Optional[ast.AST]:
1✔
234
        """Eliminate dead code from except try bodies."""
235
        new_node = self.generic_visit(node)
1✔
236
        assert isinstance(new_node, ast.Try)
1✔
237
        return ast.copy_location(
1✔
238
            ast.Try(
239
                body=_filter_dead_code(new_node.body),
240
                handlers=new_node.handlers,
241
                orelse=_filter_dead_code(new_node.orelse),
242
                finalbody=_filter_dead_code(new_node.finalbody),
243
            ),
244
            new_node,
245
        )
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