Skip to content

Commit e5187fa

Browse files
authored
More flexible resolution of import and require symbols (#547)
This commit introduces the concept of a "context boundary" Symbol Table into the analyzer. Such Symbol Tables are defined at the top level of execution contexts such as modules (namespaces), functions (sync and async), and methods. Because imports are globally available once they have been called, we hoist imported symbols to the nearest context boundary Symbol Table, which is considerably different from the standard `let` style case, where a name becomes unavailable after it goes out of scope.
1 parent a88eb51 commit e5187fa

File tree

6 files changed

+255
-51
lines changed

6 files changed

+255
-51
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
* Fixed a bug where `deftype` forms could not be declared without at least one field (#540)
2222
* Fixed a bug where not all builtin Basilisp types could be pickled (#518)
2323
* Fixed a bug where `deftype` forms could not be created interfaces declared not at the top-level of a code block in a namespace (#376)
24+
* Fixed multiple bugs relating to symbol resolution of `import`ed symbols in various contexts (#544)
2425

2526
## [v0.1.dev13] - 2020-03-16
2627
### Added

src/basilisp/core.lpy

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3712,9 +3712,19 @@
37123712
(ns-resolve *ns* sym))
37133713

37143714
(defmacro import
3715-
"Import a Python module by name."
3716-
[module]
3717-
`(import* ~module))
3715+
"Import Python modules by name.
3716+
3717+
Modules may be specified either as symbols naming the full module path or as a
3718+
vector taking the form `[full.module.path :as alias]`.
3719+
3720+
Note that unlike in Python, `import`ed Python module names are always hoisted to
3721+
the current Namespace, so imported names will be available within a Namespace even
3722+
if the `import` itself occurs within a function or method.
3723+
3724+
Use of `import` directly is discouraged in favor of the `:import` directive in
3725+
the `ns` macro."
3726+
[& modules]
3727+
`(import* ~@modules))
37183728

37193729
(import importlib)
37203730

src/basilisp/lang/compiler/analyzer.py

Lines changed: 97 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ def context(self) -> LocalType:
190190
@attr.s(auto_attribs=True, slots=True)
191191
class SymbolTable:
192192
name: str
193+
_is_context_boundary: bool = False
193194
_parent: Optional["SymbolTable"] = None
194195
_table: MutableMapping[sym.Symbol, SymbolTableEntry] = attr.ib(factory=dict)
195-
_children: MutableMapping[str, "SymbolTable"] = attr.ib(factory=dict)
196196

197197
def new_symbol(
198198
self, s: sym.Symbol, binding: Binding, warn_if_unused: bool = True
@@ -257,24 +257,19 @@ def _warn_unused_names(self):
257257
f"symbol '{entry.symbol}' defined but not used ({ns}{code_loc})"
258258
)
259259

260-
def append_frame(self, name: str, parent: "SymbolTable" = None) -> "SymbolTable":
261-
new_frame = SymbolTable(name, parent=parent)
262-
self._children[name] = new_frame
263-
return new_frame
264-
265-
def pop_frame(self, name: str) -> None:
266-
del self._children[name]
267-
268260
@contextlib.contextmanager
269-
def new_frame(self, name, warn_on_unused_names):
261+
def new_frame(
262+
self, name: str, is_context_boundary: bool, warn_on_unused_names: bool
263+
):
270264
"""Context manager for creating a new stack frame. If warn_on_unused_names is
271265
True and the logger is enabled for WARNING, call _warn_unused_names() on the
272266
child SymbolTable before it is popped."""
273-
new_frame = self.append_frame(name, parent=self)
267+
new_frame = SymbolTable(
268+
name, is_context_boundary=is_context_boundary, parent=self
269+
)
274270
yield new_frame
275271
if warn_on_unused_names and logger.isEnabledFor(logging.WARNING):
276272
new_frame._warn_unused_names()
277-
self.pop_frame(name)
278273

279274
def _as_env_map(self) -> MutableMapping[sym.Symbol, lmap.Map]:
280275
locals_ = {} if self._parent is None else self._parent._as_env_map()
@@ -286,6 +281,26 @@ def as_env_map(self) -> lmap.Map:
286281
local symbol table as of this call."""
287282
return lmap.map(self._as_env_map())
288283

284+
@property
285+
def context_boundary(self) -> "SymbolTable":
286+
"""Return the nearest context boundary parent symbol table to this one. If the
287+
current table is a context boundary, it will be returned directly.
288+
289+
Context boundary symbol tables are symbol tables defined at the top level for
290+
major Python execution boundaries, such as modules (namespaces), functions
291+
(sync and async), and methods.
292+
293+
Certain symbols (such as imports) are globally available in the execution
294+
context they are defined in once they have been created, context boundary
295+
symbol tables serve as the anchor points where we hoist these global symbols
296+
so they do not go out of scope when the local table frame is popped."""
297+
if self._is_context_boundary:
298+
return self
299+
assert (
300+
self._parent is not None
301+
), "Top symbol table must always be a context boundary"
302+
return self._parent.context_boundary
303+
289304

290305
class AnalyzerContext:
291306
__slots__ = (
@@ -318,7 +333,7 @@ def __init__(
318333
)
319334
self._recur_points: Deque[RecurPoint] = collections.deque([])
320335
self._should_macroexpand = should_macroexpand
321-
self._st = collections.deque([SymbolTable("<Top>")])
336+
self._st = collections.deque([SymbolTable("<Top>", is_context_boundary=True)])
322337
self._syntax_pos = collections.deque([NodeSyntacticPosition.EXPR])
323338

324339
@property
@@ -468,6 +483,7 @@ def put_new_symbol( # pylint: disable=too-many-arguments
468483
warn_on_shadowed_name: bool = True,
469484
warn_on_shadowed_var: bool = True,
470485
warn_if_unused: bool = True,
486+
symbol_table: Optional[SymbolTable] = None,
471487
):
472488
"""Add a new symbol to the symbol table.
473489
@@ -492,7 +508,7 @@ def put_new_symbol( # pylint: disable=too-many-arguments
492508
If WARN_ON_SHADOWED_VAR compiler option is active and the
493509
warn_on_shadowed_var keyword argument is True, then a warning will be
494510
emitted if a named var is shadowed by a local name."""
495-
st = self.symbol_table
511+
st = symbol_table or self.symbol_table
496512
no_warn_on_shadow = (
497513
Maybe(s.meta)
498514
.map(lambda m: m.val_at(SYM_NO_WARN_ON_SHADOW_META_KEY, False))
@@ -515,9 +531,11 @@ def put_new_symbol( # pylint: disable=too-many-arguments
515531
st.new_symbol(s, binding, warn_if_unused=warn_if_unused)
516532

517533
@contextlib.contextmanager
518-
def new_symbol_table(self, name):
534+
def new_symbol_table(self, name: str, is_context_boundary: bool = False):
519535
old_st = self.symbol_table
520-
with old_st.new_frame(name, self.warn_on_unused_names) as st:
536+
with old_st.new_frame(
537+
name, is_context_boundary, self.warn_on_unused_names,
538+
) as st:
521539
self._st.append(st)
522540
yield st
523541
self._st.pop()
@@ -1019,7 +1037,9 @@ def __deftype_classmethod(
10191037
kwarg_support: Optional[KeywordArgSupport] = None,
10201038
) -> DefTypeClassMethodArity:
10211039
"""Emit a node for a :classmethod member of a deftype* form."""
1022-
with ctx.hide_parent_symbol_table(), ctx.new_symbol_table(method_name):
1040+
with ctx.hide_parent_symbol_table(), ctx.new_symbol_table(
1041+
method_name, is_context_boundary=True
1042+
):
10231043
try:
10241044
cls_arg = args[0]
10251045
except IndexError:
@@ -1076,7 +1096,7 @@ def __deftype_method(
10761096
kwarg_support: Optional[KeywordArgSupport] = None,
10771097
) -> DefTypeMethodArity:
10781098
"""Emit a node for a method member of a deftype* form."""
1079-
with ctx.new_symbol_table(method_name):
1099+
with ctx.new_symbol_table(method_name, is_context_boundary=True):
10801100
try:
10811101
this_arg = args[0]
10821102
except IndexError:
@@ -1136,7 +1156,7 @@ def __deftype_property(
11361156
args: vec.Vector,
11371157
) -> DefTypeProperty:
11381158
"""Emit a node for a :property member of a deftype* form."""
1139-
with ctx.new_symbol_table(method_name):
1159+
with ctx.new_symbol_table(method_name, is_context_boundary=True):
11401160
try:
11411161
this_arg = args[0]
11421162
except IndexError:
@@ -1196,7 +1216,9 @@ def __deftype_staticmethod(
11961216
kwarg_support: Optional[KeywordArgSupport] = None,
11971217
) -> DefTypeStaticMethodArity:
11981218
"""Emit a node for a :staticmethod member of a deftype* form."""
1199-
with ctx.hide_parent_symbol_table(), ctx.new_symbol_table(method_name):
1219+
with ctx.hide_parent_symbol_table(), ctx.new_symbol_table(
1220+
method_name, is_context_boundary=True
1221+
):
12001222
has_vargs, fixed_arity, param_nodes = __deftype_method_param_bindings(ctx, args)
12011223
with ctx.new_func_ctx(FunctionContext.STATICMETHOD), ctx.expr_pos():
12021224
stmts, ret = _body_ast(ctx, runtime.nthrest(form, 2))
@@ -1663,7 +1685,7 @@ def __fn_method_ast( # pylint: disable=too-many-branches,too-many-locals
16631685
fnname: Optional[sym.Symbol] = None,
16641686
is_async: bool = False,
16651687
) -> FnArity:
1666-
with ctx.new_symbol_table("fn-method"):
1688+
with ctx.new_symbol_table("fn-method", is_context_boundary=True):
16671689
params = form.first
16681690
if not isinstance(params, vec.Vector):
16691691
raise AnalyzerException(
@@ -1772,7 +1794,7 @@ def _fn_ast( # pylint: disable=too-many-branches
17721794

17731795
idx = 1
17741796

1775-
with ctx.new_symbol_table("fn"):
1797+
with ctx.new_symbol_table("fn", is_context_boundary=True):
17761798
try:
17771799
name = runtime.nth(form, idx)
17781800
except IndexError:
@@ -2063,6 +2085,17 @@ def _import_ast( # pylint: disable=too-many-branches
20632085
if isinstance(f, sym.Symbol):
20642086
module_name = f
20652087
module_alias = None
2088+
2089+
ctx.put_new_symbol(
2090+
module_name,
2091+
Binding(
2092+
form=module_name,
2093+
name=module_name.name,
2094+
local=LocalType.IMPORT,
2095+
env=ctx.get_node_env(),
2096+
),
2097+
symbol_table=ctx.symbol_table.context_boundary,
2098+
)
20662099
elif isinstance(f, vec.Vector):
20672100
if len(f) != 3:
20682101
raise AnalyzerException(
@@ -2077,6 +2110,17 @@ def _import_ast( # pylint: disable=too-many-branches
20772110
if not isinstance(module_alias_sym, sym.Symbol):
20782111
raise AnalyzerException("Python module alias must be a symbol", form=f)
20792112
module_alias = module_alias_sym.name
2113+
2114+
ctx.put_new_symbol(
2115+
module_alias_sym,
2116+
Binding(
2117+
form=module_alias_sym,
2118+
name=module_alias,
2119+
local=LocalType.IMPORT,
2120+
env=ctx.get_node_env(),
2121+
),
2122+
symbol_table=ctx.symbol_table.context_boundary,
2123+
)
20802124
else:
20812125
raise AnalyzerException("symbol or vector expected for import*", form=f)
20822126

@@ -2893,8 +2937,28 @@ def __resolve_namespaced_symbol( # pylint: disable=too-many-branches # noqa: M
28932937
elif ctx.should_allow_unresolved_symbols:
28942938
return _const_node(ctx, form)
28952939

2940+
# Imports and requires nested in function definitions, method definitions, and
2941+
# `(do ...)` forms are not statically resolvable, since they haven't necessarily
2942+
# been imported and we want to minimize side-effecting from the compiler. In these
2943+
# cases, we merely verify that we've seen the symbol before and defer to runtime
2944+
# checks by the Python VM to verify that the import or require is legitimate.
2945+
maybe_import_or_require_sym = sym.symbol(form.ns)
2946+
maybe_import_or_require_entry = ctx.symbol_table.find_symbol(
2947+
maybe_import_or_require_sym
2948+
)
2949+
if maybe_import_or_require_entry is not None:
2950+
if maybe_import_or_require_entry.context == LocalType.IMPORT:
2951+
ctx.symbol_table.mark_used(maybe_import_or_require_sym)
2952+
return MaybeHostForm(
2953+
form=form,
2954+
class_=munge(form.ns),
2955+
field=munge(form.name),
2956+
target=None,
2957+
env=ctx.get_node_env(pos=ctx.syntax_position),
2958+
)
2959+
28962960
# Static and class methods on types in the current namespace can be referred
2897-
# to as `Type/static-method`. In these casess, we will try to resolve the
2961+
# to as `Type/static-method`. In these cases, we will try to resolve the
28982962
# namespace portion of the symbol as a Var within the current namespace.
28992963
maybe_type_or_class = current_ns.find(sym.symbol(form.ns))
29002964
if maybe_type_or_class is not None:
@@ -2959,6 +3023,16 @@ def __resolve_bare_symbol(
29593023
env=ctx.get_node_env(pos=ctx.syntax_position),
29603024
)
29613025

3026+
# Allow users to resolve imported module names directly
3027+
maybe_import = current_ns.get_import(form)
3028+
if maybe_import is not None:
3029+
return MaybeClass(
3030+
form=form,
3031+
class_=munge(form.name),
3032+
target=maybe_import,
3033+
env=ctx.get_node_env(pos=ctx.syntax_position),
3034+
)
3035+
29623036
if ctx.should_allow_unresolved_symbols:
29633037
return _const_node(ctx, form)
29643038

0 commit comments

Comments
 (0)