@@ -190,9 +190,9 @@ def context(self) -> LocalType:
190
190
@attr .s (auto_attribs = True , slots = True )
191
191
class SymbolTable :
192
192
name : str
193
+ _is_context_boundary : bool = False
193
194
_parent : Optional ["SymbolTable" ] = None
194
195
_table : MutableMapping [sym .Symbol , SymbolTableEntry ] = attr .ib (factory = dict )
195
- _children : MutableMapping [str , "SymbolTable" ] = attr .ib (factory = dict )
196
196
197
197
def new_symbol (
198
198
self , s : sym .Symbol , binding : Binding , warn_if_unused : bool = True
@@ -257,24 +257,19 @@ def _warn_unused_names(self):
257
257
f"symbol '{ entry .symbol } ' defined but not used ({ ns } { code_loc } )"
258
258
)
259
259
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
-
268
260
@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
+ ):
270
264
"""Context manager for creating a new stack frame. If warn_on_unused_names is
271
265
True and the logger is enabled for WARNING, call _warn_unused_names() on the
272
266
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
+ )
274
270
yield new_frame
275
271
if warn_on_unused_names and logger .isEnabledFor (logging .WARNING ):
276
272
new_frame ._warn_unused_names ()
277
- self .pop_frame (name )
278
273
279
274
def _as_env_map (self ) -> MutableMapping [sym .Symbol , lmap .Map ]:
280
275
locals_ = {} if self ._parent is None else self ._parent ._as_env_map ()
@@ -286,6 +281,26 @@ def as_env_map(self) -> lmap.Map:
286
281
local symbol table as of this call."""
287
282
return lmap .map (self ._as_env_map ())
288
283
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
+
289
304
290
305
class AnalyzerContext :
291
306
__slots__ = (
@@ -318,7 +333,7 @@ def __init__(
318
333
)
319
334
self ._recur_points : Deque [RecurPoint ] = collections .deque ([])
320
335
self ._should_macroexpand = should_macroexpand
321
- self ._st = collections .deque ([SymbolTable ("<Top>" )])
336
+ self ._st = collections .deque ([SymbolTable ("<Top>" , is_context_boundary = True )])
322
337
self ._syntax_pos = collections .deque ([NodeSyntacticPosition .EXPR ])
323
338
324
339
@property
@@ -468,6 +483,7 @@ def put_new_symbol( # pylint: disable=too-many-arguments
468
483
warn_on_shadowed_name : bool = True ,
469
484
warn_on_shadowed_var : bool = True ,
470
485
warn_if_unused : bool = True ,
486
+ symbol_table : Optional [SymbolTable ] = None ,
471
487
):
472
488
"""Add a new symbol to the symbol table.
473
489
@@ -492,7 +508,7 @@ def put_new_symbol( # pylint: disable=too-many-arguments
492
508
If WARN_ON_SHADOWED_VAR compiler option is active and the
493
509
warn_on_shadowed_var keyword argument is True, then a warning will be
494
510
emitted if a named var is shadowed by a local name."""
495
- st = self .symbol_table
511
+ st = symbol_table or self .symbol_table
496
512
no_warn_on_shadow = (
497
513
Maybe (s .meta )
498
514
.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
515
531
st .new_symbol (s , binding , warn_if_unused = warn_if_unused )
516
532
517
533
@contextlib .contextmanager
518
- def new_symbol_table (self , name ):
534
+ def new_symbol_table (self , name : str , is_context_boundary : bool = False ):
519
535
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 :
521
539
self ._st .append (st )
522
540
yield st
523
541
self ._st .pop ()
@@ -1019,7 +1037,9 @@ def __deftype_classmethod(
1019
1037
kwarg_support : Optional [KeywordArgSupport ] = None ,
1020
1038
) -> DefTypeClassMethodArity :
1021
1039
"""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
+ ):
1023
1043
try :
1024
1044
cls_arg = args [0 ]
1025
1045
except IndexError :
@@ -1076,7 +1096,7 @@ def __deftype_method(
1076
1096
kwarg_support : Optional [KeywordArgSupport ] = None ,
1077
1097
) -> DefTypeMethodArity :
1078
1098
"""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 ):
1080
1100
try :
1081
1101
this_arg = args [0 ]
1082
1102
except IndexError :
@@ -1136,7 +1156,7 @@ def __deftype_property(
1136
1156
args : vec .Vector ,
1137
1157
) -> DefTypeProperty :
1138
1158
"""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 ):
1140
1160
try :
1141
1161
this_arg = args [0 ]
1142
1162
except IndexError :
@@ -1196,7 +1216,9 @@ def __deftype_staticmethod(
1196
1216
kwarg_support : Optional [KeywordArgSupport ] = None ,
1197
1217
) -> DefTypeStaticMethodArity :
1198
1218
"""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
+ ):
1200
1222
has_vargs , fixed_arity , param_nodes = __deftype_method_param_bindings (ctx , args )
1201
1223
with ctx .new_func_ctx (FunctionContext .STATICMETHOD ), ctx .expr_pos ():
1202
1224
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
1663
1685
fnname : Optional [sym .Symbol ] = None ,
1664
1686
is_async : bool = False ,
1665
1687
) -> FnArity :
1666
- with ctx .new_symbol_table ("fn-method" ):
1688
+ with ctx .new_symbol_table ("fn-method" , is_context_boundary = True ):
1667
1689
params = form .first
1668
1690
if not isinstance (params , vec .Vector ):
1669
1691
raise AnalyzerException (
@@ -1772,7 +1794,7 @@ def _fn_ast( # pylint: disable=too-many-branches
1772
1794
1773
1795
idx = 1
1774
1796
1775
- with ctx .new_symbol_table ("fn" ):
1797
+ with ctx .new_symbol_table ("fn" , is_context_boundary = True ):
1776
1798
try :
1777
1799
name = runtime .nth (form , idx )
1778
1800
except IndexError :
@@ -2063,6 +2085,17 @@ def _import_ast( # pylint: disable=too-many-branches
2063
2085
if isinstance (f , sym .Symbol ):
2064
2086
module_name = f
2065
2087
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
+ )
2066
2099
elif isinstance (f , vec .Vector ):
2067
2100
if len (f ) != 3 :
2068
2101
raise AnalyzerException (
@@ -2077,6 +2110,17 @@ def _import_ast( # pylint: disable=too-many-branches
2077
2110
if not isinstance (module_alias_sym , sym .Symbol ):
2078
2111
raise AnalyzerException ("Python module alias must be a symbol" , form = f )
2079
2112
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
+ )
2080
2124
else :
2081
2125
raise AnalyzerException ("symbol or vector expected for import*" , form = f )
2082
2126
@@ -2893,8 +2937,28 @@ def __resolve_namespaced_symbol( # pylint: disable=too-many-branches # noqa: M
2893
2937
elif ctx .should_allow_unresolved_symbols :
2894
2938
return _const_node (ctx , form )
2895
2939
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
+
2896
2960
# 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
2898
2962
# namespace portion of the symbol as a Var within the current namespace.
2899
2963
maybe_type_or_class = current_ns .find (sym .symbol (form .ns ))
2900
2964
if maybe_type_or_class is not None :
@@ -2959,6 +3023,16 @@ def __resolve_bare_symbol(
2959
3023
env = ctx .get_node_env (pos = ctx .syntax_position ),
2960
3024
)
2961
3025
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
+
2962
3036
if ctx .should_allow_unresolved_symbols :
2963
3037
return _const_node (ctx , form )
2964
3038
0 commit comments