Skip to content

Commit 86c59ea

Browse files
authored
Unroll top-level do forms (#1061)
Fixes #1028 `do` forms emitted by macros are still not completely unrolled.
1 parent d002d1a commit 86c59ea

File tree

5 files changed

+284
-82
lines changed

5 files changed

+284
-82
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

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

1617
### Fixed
1718
* Fix a bug where Basilisp did not respect the value of Python's `sys.dont_write_bytecode` flag when generating bytecode (#1054)

docs/runtime.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,27 @@ This is roughly analogous to the Java classpath in Clojure.
111111
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.
112112
These values may also be set via :ref:`cli` arguments.
113113

114+
Requiring Code Dynamically
115+
##########################
116+
117+
The Basilisp compiler attempts to verify the existence of Vars and Python module members during its analysis phase.
118+
It typically does that by introspecting the runtime environment (Namespaces or Python modules).
119+
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).
120+
121+
.. code-block::
122+
123+
((fn []
124+
(require 'basilisp.set)
125+
(basilisp.set/difference #{:b} #{:a})))
126+
;; => error occurred during macroexpansion: unable to resolve symbol 'basilisp.set/difference' in this context
127+
128+
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:
129+
130+
.. code-block::
131+
132+
((fn []
133+
((requiring-resolve 'basilisp.set/difference) #{:b} #{:a}))) ;; => #{:b}
134+
114135
.. _namespace_imports:
115136

116137
Namespace Imports

src/basilisp/core.lpy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5068,7 +5068,7 @@
50685068
.. warning::
50695069

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

50745074
- Vars will be re-``def``'ed based on the current definition of the underlying

src/basilisp/lang/compiler/__init__.py

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
macroexpand,
2424
macroexpand_1,
2525
)
26+
from basilisp.lang.compiler.constants import SpecialForm
2627
from basilisp.lang.compiler.exception import CompilerException, CompilerPhase # noqa
2728
from basilisp.lang.compiler.generator import (
2829
USE_VAR_INDIRECTION,
@@ -34,6 +35,7 @@
3435
from basilisp.lang.compiler.generator import gen_py_ast, py_module_preamble
3536
from basilisp.lang.compiler.generator import statementize as _statementize
3637
from basilisp.lang.compiler.optimizer import PythonASTOptimizer
38+
from basilisp.lang.interfaces import ISeq
3739
from basilisp.lang.typing import CompilerOpts, ReaderForm
3840
from basilisp.lang.util import genname
3941
from basilisp.util import Maybe
@@ -148,6 +150,18 @@ def _emit_ast_string(
148150
runtime.add_generated_python(to_py_str(module), which_ns=ns)
149151

150152

153+
def _flatmap_forms(forms: Iterable[ReaderForm]) -> Iterable[ReaderForm]:
154+
"""Flatmap over an iterable of forms, unrolling any top-level `do` forms"""
155+
for form in forms:
156+
if isinstance(form, ISeq) and form.first == SpecialForm.DO:
157+
yield from _flatmap_forms(form.rest)
158+
else:
159+
yield form
160+
161+
162+
_sentinel = object()
163+
164+
151165
def compile_and_exec_form(
152166
form: ReaderForm,
153167
ctx: CompilerContext,
@@ -166,34 +180,42 @@ def compile_and_exec_form(
166180
if not ns.module.__basilisp_bootstrapped__:
167181
_bootstrap_module(ctx.generator_context, ctx.py_ast_optimizer, ns)
168182

169-
final_wrapped_name = genname(wrapped_fn_name)
170-
171-
lisp_ast = analyze_form(ctx.analyzer_context, form)
172-
py_ast = gen_py_ast(ctx.generator_context, lisp_ast)
173-
form_ast = list(
174-
map(
175-
_statementize,
176-
itertools.chain(
177-
py_ast.dependencies,
178-
[_expressionize(GeneratedPyAST(node=py_ast.node), final_wrapped_name)],
179-
),
183+
last = _sentinel
184+
for unrolled_form in _flatmap_forms([form]):
185+
final_wrapped_name = genname(wrapped_fn_name)
186+
lisp_ast = analyze_form(ctx.analyzer_context, unrolled_form)
187+
py_ast = gen_py_ast(ctx.generator_context, lisp_ast)
188+
form_ast = list(
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+
)
180200
)
181-
)
182201

183-
ast_module = ast.Module(body=form_ast, type_ignores=[])
184-
ast_module = ctx.py_ast_optimizer.visit(ast_module)
185-
ast.fix_missing_locations(ast_module)
202+
ast_module = ast.Module(body=form_ast, type_ignores=[])
203+
ast_module = ctx.py_ast_optimizer.visit(ast_module)
204+
ast.fix_missing_locations(ast_module)
186205

187-
_emit_ast_string(ns, ast_module)
206+
_emit_ast_string(ns, ast_module)
188207

189-
bytecode = compile(ast_module, ctx.filename, "exec")
190-
if collect_bytecode:
191-
collect_bytecode(bytecode)
192-
exec(bytecode, ns.module.__dict__) # pylint: disable=exec-used
193-
try:
194-
return getattr(ns.module, final_wrapped_name)()
195-
finally:
196-
del ns.module.__dict__[final_wrapped_name]
208+
bytecode = compile(ast_module, ctx.filename, "exec")
209+
if collect_bytecode:
210+
collect_bytecode(bytecode)
211+
exec(bytecode, ns.module.__dict__) # pylint: disable=exec-used
212+
try:
213+
last = getattr(ns.module, final_wrapped_name)()
214+
finally:
215+
del ns.module.__dict__[final_wrapped_name]
216+
217+
assert last is not _sentinel, "Must compile at least one form"
218+
return last
197219

198220

199221
def _incremental_compile_module(
@@ -257,7 +279,7 @@ def compile_module(
257279
"""
258280
_bootstrap_module(ctx.generator_context, ctx.py_ast_optimizer, ns)
259281

260-
for form in forms:
282+
for form in _flatmap_forms(forms):
261283
nodes = gen_py_ast(
262284
ctx.generator_context, analyze_form(ctx.analyzer_context, form)
263285
)

0 commit comments

Comments
 (0)