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

basilisp-lang / basilisp / 10943311926

19 Sep 2024 02:41PM UTC coverage: 98.89%. Remained the same
10943311926

Pull #1062

github

web-flow
Merge 1e881487b into 86c59ead1
Pull Request #1062: Prepare for release v0.2.3

1903 of 1910 branches covered (99.63%)

Branch coverage included in aggregate %.

8695 of 8807 relevant lines covered (98.73%)

0.99 hits per line

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

96.18
/src/basilisp/lang/compiler/__init__.py
1
import ast
1✔
2
import itertools
1✔
3
import os
1✔
4
import sys
1✔
5
import types
1✔
6
from pathlib import Path
1✔
7
from typing import Any, Callable, Iterable, List, Optional
1✔
8

9
from basilisp.lang import list as llist
1✔
10
from basilisp.lang import map as lmap
1✔
11
from basilisp.lang import runtime as runtime
1✔
12
from basilisp.lang import symbol as sym
1✔
13
from basilisp.lang.compiler.analyzer import (  # noqa
1✔
14
    GENERATE_AUTO_INLINES,
15
    INLINE_FUNCTIONS,
16
    WARN_ON_ARITY_MISMATCH,
17
    WARN_ON_NON_DYNAMIC_SET,
18
    WARN_ON_SHADOWED_NAME,
19
    WARN_ON_SHADOWED_VAR,
20
    WARN_ON_UNUSED_NAMES,
21
    AnalyzerContext,
22
    analyze_form,
23
    macroexpand,
24
    macroexpand_1,
25
)
26
from basilisp.lang.compiler.constants import SpecialForm
1✔
27
from basilisp.lang.compiler.exception import CompilerException, CompilerPhase  # noqa
1✔
28
from basilisp.lang.compiler.generator import (
1✔
29
    USE_VAR_INDIRECTION,
30
    WARN_ON_VAR_INDIRECTION,
31
    GeneratedPyAST,
32
    GeneratorContext,
33
)
34
from basilisp.lang.compiler.generator import expressionize as _expressionize  # noqa
1✔
35
from basilisp.lang.compiler.generator import gen_py_ast, py_module_preamble
1✔
36
from basilisp.lang.compiler.generator import statementize as _statementize
1✔
37
from basilisp.lang.compiler.optimizer import PythonASTOptimizer
1✔
38
from basilisp.lang.interfaces import ISeq
1✔
39
from basilisp.lang.typing import CompilerOpts, ReaderForm
1✔
40
from basilisp.lang.util import genname
1✔
41
from basilisp.util import Maybe
1✔
42

43
_DEFAULT_FN = "__lisp_expr__"
1✔
44

45

46
if sys.version_info >= (3, 9):
1✔
47
    from ast import unparse
1✔
48

49
    def to_py_str(t: ast.AST) -> str:
1✔
50
        """Return a string of the Python code which would generate the input
51
        AST node."""
52
        return unparse(t) + "\n\n"
1✔
53

54
else:
55
    try:
1✔
56
        from astor import code_gen as codegen
1✔
57

58
        def to_py_str(t: ast.AST) -> str:
1✔
59
            """Return a string of the Python code which would generate the input
60
            AST node."""
61
            return codegen.to_source(t)
1✔
62

63
    except ImportError:
×
64
        import warnings
×
65

66
        def to_py_str(t: ast.AST) -> str:  # pylint: disable=unused-argument
×
67
            warnings.warn(
×
68
                "Unable to generate Python code from generated AST due to missing "
69
                "dependency 'astor'",
70
                RuntimeWarning,
71
            )
72
            return ""
×
73

74

75
BytecodeCollector = Callable[[types.CodeType], None]
1✔
76

77

78
class CompilerContext:
1✔
79
    __slots__ = ("_filename", "_actx", "_gctx", "_optimizer")
1✔
80

81
    def __init__(self, filename: str, opts: Optional[CompilerOpts] = None):
1✔
82
        self._filename = filename
1✔
83
        self._actx = AnalyzerContext(filename=filename, opts=opts)
1✔
84
        self._gctx = GeneratorContext(filename=filename, opts=opts)
1✔
85
        self._optimizer = PythonASTOptimizer()
1✔
86

87
    @property
1✔
88
    def filename(self) -> str:
1✔
89
        return self._filename
1✔
90

91
    @property
1✔
92
    def analyzer_context(self) -> AnalyzerContext:
1✔
93
        return self._actx
1✔
94

95
    @property
1✔
96
    def generator_context(self) -> GeneratorContext:
1✔
97
        return self._gctx
1✔
98

99
    @property
1✔
100
    def py_ast_optimizer(self) -> PythonASTOptimizer:
1✔
101
        return self._optimizer
1✔
102

103

104
def compiler_opts(  # pylint: disable=too-many-arguments
1✔
105
    generate_auto_inlines: Optional[bool] = None,
106
    inline_functions: Optional[bool] = None,
107
    warn_on_arity_mismatch: Optional[bool] = None,
108
    warn_on_shadowed_name: Optional[bool] = None,
109
    warn_on_shadowed_var: Optional[bool] = None,
110
    warn_on_unused_names: Optional[bool] = None,
111
    warn_on_non_dynamic_set: Optional[bool] = None,
112
    use_var_indirection: Optional[bool] = None,
113
    warn_on_var_indirection: Optional[bool] = None,
114
) -> CompilerOpts:
115
    """Return a map of compiler options with defaults applied."""
116
    return lmap.map(
1✔
117
        {
118
            # Analyzer options
119
            GENERATE_AUTO_INLINES: Maybe(generate_auto_inlines).or_else_get(True),
120
            INLINE_FUNCTIONS: Maybe(inline_functions).or_else_get(True),
121
            WARN_ON_ARITY_MISMATCH: Maybe(warn_on_arity_mismatch).or_else_get(True),
122
            WARN_ON_SHADOWED_NAME: Maybe(warn_on_shadowed_name).or_else_get(False),
123
            WARN_ON_SHADOWED_VAR: Maybe(warn_on_shadowed_var).or_else_get(False),
124
            WARN_ON_UNUSED_NAMES: Maybe(warn_on_unused_names).or_else_get(True),
125
            WARN_ON_NON_DYNAMIC_SET: Maybe(warn_on_non_dynamic_set).or_else_get(True),
126
            # Generator options
127
            USE_VAR_INDIRECTION: Maybe(use_var_indirection).or_else_get(False),
128
            WARN_ON_VAR_INDIRECTION: Maybe(warn_on_var_indirection).or_else_get(True),
129
        }
130
    )
131

132

133
def _emit_ast_string(
134
    ns: runtime.Namespace,
135
    module: ast.AST,
136
) -> None:  # pragma: no cover
137
    """Emit the generated Python AST string either to standard out or to the
138
    *generated-python* dynamic Var for the current namespace. If the
139
    BASILISP_EMIT_GENERATED_PYTHON env var is not set True, this method is a
140
    no-op."""
141
    # TODO: eventually, this default should become "false" but during this
142
    #       period of heavy development, having it set to "true" by default
143
    #       is tremendously useful
144
    if os.getenv("BASILISP_EMIT_GENERATED_PYTHON", "true").lower() != "true":
145
        return
146

147
    if runtime.print_generated_python():
148
        print(to_py_str(module))
149
    else:
150
        runtime.add_generated_python(to_py_str(module), which_ns=ns)
151

152

153
def _flatmap_forms(forms: Iterable[ReaderForm]) -> Iterable[ReaderForm]:
1✔
154
    """Flatmap over an iterable of forms, unrolling any top-level `do` forms"""
155
    for form in forms:
1✔
156
        if isinstance(form, ISeq) and form.first == SpecialForm.DO:
1✔
157
            yield from _flatmap_forms(form.rest)
1✔
158
        else:
159
            yield form
1✔
160

161

162
_sentinel = object()
1✔
163

164

165
def compile_and_exec_form(
1✔
166
    form: ReaderForm,
167
    ctx: CompilerContext,
168
    ns: runtime.Namespace,
169
    wrapped_fn_name: str = _DEFAULT_FN,
170
    collect_bytecode: Optional[BytecodeCollector] = None,
171
) -> Any:
172
    """Compile and execute the given form. This function will be most useful
173
    for the REPL and testing purposes. Returns the result of the executed expression.
174

175
    Callers may override the wrapped function name, which is used by the
176
    REPL to evaluate the result of an expression and print it back out."""
177
    if form is None:
1✔
178
        return None
1✔
179

180
    if not ns.module.__basilisp_bootstrapped__:
1✔
181
        _bootstrap_module(ctx.generator_context, ctx.py_ast_optimizer, ns)
1✔
182

183
    last = _sentinel
1✔
184
    for unrolled_form in _flatmap_forms([form]):
1✔
185
        final_wrapped_name = genname(wrapped_fn_name)
1✔
186
        lisp_ast = analyze_form(ctx.analyzer_context, unrolled_form)
1✔
187
        py_ast = gen_py_ast(ctx.generator_context, lisp_ast)
1✔
188
        form_ast = list(
1✔
189
            map(
190
                _statementize,
191
                itertools.chain(
192
                    py_ast.dependencies,
193
                    [
194
                        _expressionize(
195
                            GeneratedPyAST(node=py_ast.node), final_wrapped_name
196
                        )
197
                    ],
198
                ),
199
            )
200
        )
201

202
        ast_module = ast.Module(body=form_ast, type_ignores=[])
1✔
203
        ast_module = ctx.py_ast_optimizer.visit(ast_module)
1✔
204
        ast.fix_missing_locations(ast_module)
1✔
205

206
        _emit_ast_string(ns, ast_module)
1✔
207

208
        bytecode = compile(ast_module, ctx.filename, "exec")
1✔
209
        if collect_bytecode:
1✔
210
            collect_bytecode(bytecode)
1✔
211
        exec(bytecode, ns.module.__dict__)  # pylint: disable=exec-used
1✔
212
        try:
1✔
213
            last = getattr(ns.module, final_wrapped_name)()
1✔
214
        finally:
215
            del ns.module.__dict__[final_wrapped_name]
1✔
216

217
    assert last is not _sentinel, "Must compile at least one form"
1✔
218
    return last
1✔
219

220

221
def _incremental_compile_module(
1✔
222
    optimizer: PythonASTOptimizer,
223
    py_ast: GeneratedPyAST,
224
    ns: runtime.Namespace,
225
    source_filename: str,
226
    collect_bytecode: Optional[BytecodeCollector] = None,
227
) -> None:
228
    """Incrementally compile a stream of AST nodes in module mod.
229

230
    The source_filename will be passed to Python's native compile.
231

232
    Incremental compilation is an integral part of generating a Python module
233
    during the same process as macro-expansion."""
234
    module_body = list(
1✔
235
        map(_statementize, itertools.chain(py_ast.dependencies, [py_ast.node]))
236
    )
237

238
    module = ast.Module(body=list(module_body), type_ignores=[])
1✔
239
    module = optimizer.visit(module)
1✔
240
    ast.fix_missing_locations(module)
1✔
241

242
    _emit_ast_string(ns, module)
1✔
243

244
    bytecode = compile(module, source_filename, "exec")
1✔
245
    if collect_bytecode:
1✔
246
        collect_bytecode(bytecode)
1✔
247
    exec(bytecode, ns.module.__dict__)  # pylint: disable=exec-used
1✔
248

249

250
def _bootstrap_module(
1✔
251
    gctx: GeneratorContext,
252
    optimizer: PythonASTOptimizer,
253
    ns: runtime.Namespace,
254
    collect_bytecode: Optional[BytecodeCollector] = None,
255
) -> None:
256
    """Bootstrap a new module with imports and other boilerplate."""
257
    _incremental_compile_module(
1✔
258
        optimizer,
259
        py_module_preamble(ns),
260
        ns,
261
        source_filename=gctx.filename,
262
        collect_bytecode=collect_bytecode,
263
    )
264
    ns.module.__basilisp_bootstrapped__ = True
1✔
265

266

267
def compile_module(
1✔
268
    forms: Iterable[ReaderForm],
269
    ctx: CompilerContext,
270
    ns: runtime.Namespace,
271
    collect_bytecode: Optional[BytecodeCollector] = None,
272
) -> None:
273
    """Compile an entire Basilisp module into Python bytecode which can be
274
    executed as a Python module.
275

276
    This function is designed to generate bytecode which can be used for the
277
    Basilisp import machinery, to allow callers to import Basilisp modules from
278
    Python code.
279
    """
280
    _bootstrap_module(ctx.generator_context, ctx.py_ast_optimizer, ns)
1✔
281

282
    for form in _flatmap_forms(forms):
1✔
283
        nodes = gen_py_ast(
1✔
284
            ctx.generator_context, analyze_form(ctx.analyzer_context, form)
285
        )
286
        _incremental_compile_module(
1✔
287
            ctx.py_ast_optimizer,
288
            nodes,
289
            ns,
290
            source_filename=ctx.filename,
291
            collect_bytecode=collect_bytecode,
292
        )
293

294

295
def compile_bytecode(
1✔
296
    code: List[types.CodeType],
297
    gctx: GeneratorContext,
298
    optimizer: PythonASTOptimizer,
299
    ns: runtime.Namespace,
300
) -> None:
301
    """Compile cached bytecode into the given module.
302

303
    The Basilisp import hook attempts to cache bytecode while compiling Basilisp
304
    namespaces. When the cached bytecode is reloaded from disk, it needs to be
305
    compiled within a bootstrapped module. This function bootstraps the module
306
    and then proceeds to compile a collection of bytecodes into the module."""
307
    _bootstrap_module(gctx, optimizer, ns)
1✔
308
    for bytecode in code:
1✔
309
        exec(bytecode, ns.module.__dict__)  # pylint: disable=exec-used
1✔
310

311

312
_LOAD_SYM = sym.symbol("load", ns=runtime.CORE_NS)
1✔
313
_LOAD_FILE_SYM = sym.symbol("load-file", ns=runtime.CORE_NS)
1✔
314

315

316
def load(
1✔
317
    path: str,
318
    ctx: CompilerContext,
319
    ns: runtime.Namespace,
320
    collect_bytecode: Optional[BytecodeCollector] = None,
321
) -> Any:
322
    """Call :lpy:fn:`basilisp.core/load` with the given ``path``, returning the
323
    result."""
324
    return compile_and_exec_form(
1✔
325
        llist.l(_LOAD_SYM, path), ctx, ns, collect_bytecode=collect_bytecode
326
    )
327

328

329
def load_file(
1✔
330
    path: Path,
331
    ctx: CompilerContext,
332
    ns: runtime.Namespace,
333
    collect_bytecode: Optional[BytecodeCollector] = None,
334
) -> Any:
335
    """Call :lpy:fn:`basilisp.core/load-file` with the given ``path``, returning the
336
    result."""
337
    return compile_and_exec_form(
1✔
338
        llist.l(_LOAD_FILE_SYM, path.as_posix()),
339
        ctx,
340
        ns,
341
        collect_bytecode=collect_bytecode,
342
    )
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