Skip to content

Unroll top-level do forms #1061

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 9 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
* The compiler will issue a warning when adding any alias that might conflict with any other alias (#1045)
* The compiler is now capable of unrolling top level `do` forms (not including `do` forms emitted by macros) (#1028)

### Fixed
* Fix a bug where Basilisp did not respect the value of Python's `sys.dont_write_bytecode` flag when generating bytecode (#1054)
Expand Down
21 changes: 21 additions & 0 deletions docs/runtime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,27 @@ This is roughly analogous to the Java classpath in Clojure.
These values may be set manually, but are more often configured by some project management tool such as Poetry or defined in your Python virtualenv.
These values may also be set via :ref:`cli` arguments.

Requiring Code Dynamically
##########################

The Basilisp compiler attempts to verify the existence of Vars and Python module members during its analysis phase.
It typically does that by introspecting the runtime environment (Namespaces or Python modules).
Requiring a namespace in a highly dynamic context (e.g. from within a function call) and then immediately attempting to reference that value prevents the compiler from verifying references (which is an error).

.. code-block::

((fn []
(require 'basilisp.set)
(basilisp.set/difference #{:b} #{:a})))
;; => error occurred during macroexpansion: unable to resolve symbol 'basilisp.set/difference' in this context

In such cases, it may be preferable to use :lpy:fn:`requiring-resolve` to dynamically require and resolve the Var rather than fighting the compiler:

.. code-block::

((fn []
((requiring-resolve 'basilisp.set/difference) #{:b} #{:a}))) ;; => #{:b}

.. _namespace_imports:

Namespace Imports
Expand Down
2 changes: 1 addition & 1 deletion src/basilisp/core.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -5068,7 +5068,7 @@
.. warning::

Reloading namespaces has many of the same limitations described for
:external:py:func:`importlib.reload_module`. Below is a non-exhaustive set of
:external:py:func:`importlib.reload`. Below is a non-exhaustive set of
limitations of reloading Basilisp namespace:

- Vars will be re-``def``'ed based on the current definition of the underlying
Expand Down
72 changes: 47 additions & 25 deletions src/basilisp/lang/compiler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
macroexpand,
macroexpand_1,
)
from basilisp.lang.compiler.constants import SpecialForm
from basilisp.lang.compiler.exception import CompilerException, CompilerPhase # noqa
from basilisp.lang.compiler.generator import (
USE_VAR_INDIRECTION,
Expand All @@ -34,6 +35,7 @@
from basilisp.lang.compiler.generator import gen_py_ast, py_module_preamble
from basilisp.lang.compiler.generator import statementize as _statementize
from basilisp.lang.compiler.optimizer import PythonASTOptimizer
from basilisp.lang.interfaces import ISeq
from basilisp.lang.typing import CompilerOpts, ReaderForm
from basilisp.lang.util import genname
from basilisp.util import Maybe
Expand Down Expand Up @@ -148,6 +150,18 @@ def _emit_ast_string(
runtime.add_generated_python(to_py_str(module), which_ns=ns)


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


_sentinel = object()


def compile_and_exec_form(
form: ReaderForm,
ctx: CompilerContext,
Expand All @@ -166,34 +180,42 @@ def compile_and_exec_form(
if not ns.module.__basilisp_bootstrapped__:
_bootstrap_module(ctx.generator_context, ctx.py_ast_optimizer, ns)

final_wrapped_name = genname(wrapped_fn_name)

lisp_ast = analyze_form(ctx.analyzer_context, form)
py_ast = gen_py_ast(ctx.generator_context, lisp_ast)
form_ast = list(
map(
_statementize,
itertools.chain(
py_ast.dependencies,
[_expressionize(GeneratedPyAST(node=py_ast.node), final_wrapped_name)],
),
last = _sentinel
for unrolled_form in _flatmap_forms([form]):
final_wrapped_name = genname(wrapped_fn_name)
lisp_ast = analyze_form(ctx.analyzer_context, unrolled_form)
py_ast = gen_py_ast(ctx.generator_context, lisp_ast)
form_ast = list(
map(
_statementize,
itertools.chain(
py_ast.dependencies,
[
_expressionize(
GeneratedPyAST(node=py_ast.node), final_wrapped_name
)
],
),
)
)
)

ast_module = ast.Module(body=form_ast, type_ignores=[])
ast_module = ctx.py_ast_optimizer.visit(ast_module)
ast.fix_missing_locations(ast_module)
ast_module = ast.Module(body=form_ast, type_ignores=[])
ast_module = ctx.py_ast_optimizer.visit(ast_module)
ast.fix_missing_locations(ast_module)

_emit_ast_string(ns, ast_module)
_emit_ast_string(ns, ast_module)

bytecode = compile(ast_module, ctx.filename, "exec")
if collect_bytecode:
collect_bytecode(bytecode)
exec(bytecode, ns.module.__dict__) # pylint: disable=exec-used
try:
return getattr(ns.module, final_wrapped_name)()
finally:
del ns.module.__dict__[final_wrapped_name]
bytecode = compile(ast_module, ctx.filename, "exec")
if collect_bytecode:
collect_bytecode(bytecode)
exec(bytecode, ns.module.__dict__) # pylint: disable=exec-used
try:
last = getattr(ns.module, final_wrapped_name)()
finally:
del ns.module.__dict__[final_wrapped_name]

assert last is not _sentinel, "Must compile at least one form"
return last


def _incremental_compile_module(
Expand Down Expand Up @@ -257,7 +279,7 @@ def compile_module(
"""
_bootstrap_module(ctx.generator_context, ctx.py_ast_optimizer, ns)

for form in forms:
for form in _flatmap_forms(forms):
nodes = gen_py_ast(
ctx.generator_context, analyze_form(ctx.analyzer_context, form)
)
Expand Down
Loading