@@ -281,6 +281,7 @@ class AnalyzerContext:
281
281
"_filename" ,
282
282
"_func_ctx" ,
283
283
"_is_quoted" ,
284
+ "_macro_ns" ,
284
285
"_opts" ,
285
286
"_recur_points" ,
286
287
"_should_macroexpand" ,
@@ -298,6 +299,7 @@ def __init__(
298
299
self ._filename = Maybe (filename ).or_else_get (DEFAULT_COMPILER_FILE_PATH )
299
300
self ._func_ctx : Deque [bool ] = collections .deque ([])
300
301
self ._is_quoted : Deque [bool ] = collections .deque ([])
302
+ self ._macro_ns : Deque [Optional [runtime .Namespace ]] = collections .deque ([])
301
303
self ._opts = (
302
304
Maybe (opts ).map (lmap .map ).or_else_get (lmap .Map .empty ()) # type: ignore
303
305
)
@@ -348,6 +350,35 @@ def quoted(self):
348
350
yield
349
351
self ._is_quoted .pop ()
350
352
353
+ @property
354
+ def current_macro_ns (self ) -> Optional [runtime .Namespace ]:
355
+ """Return the current transient namespace available during macroexpansion.
356
+
357
+ If None, the analyzer should only use the current namespace for symbol
358
+ resolution."""
359
+ try :
360
+ return self ._macro_ns [- 1 ]
361
+ except IndexError :
362
+ return None
363
+
364
+ @contextlib .contextmanager
365
+ def macro_ns (self , ns : Optional [runtime .Namespace ]):
366
+ """Set the transient namespace which is available to the analyer during a
367
+ macroexpansion phase.
368
+
369
+ If set to None, prohibit the analyzer from using another namespace for symbol
370
+ resolution.
371
+
372
+ During macroexpansion, new forms referenced from the macro namespace would
373
+ be unavailable to the namespace containing the original macro invocation.
374
+ The macro namespace is a temporary override pointing to the namespace of the
375
+ macro definition which can be used to resolve these transient references."""
376
+ self ._macro_ns .append (ns )
377
+ try :
378
+ yield
379
+ finally :
380
+ self ._macro_ns .pop ()
381
+
351
382
@property
352
383
def should_allow_unresolved_symbols (self ) -> bool :
353
384
"""If True, the analyzer will allow unresolved symbols. This is primarily
@@ -1639,7 +1670,10 @@ def _invoke_ast(ctx: AnalyzerContext, form: Union[llist.List, ISeq]) -> Node:
1639
1670
try :
1640
1671
macro_env = ctx .symbol_table .as_env_map ()
1641
1672
expanded = fn .var .value (macro_env , form , * form .rest )
1642
- expanded_ast = _analyze_form (ctx , expanded )
1673
+ with ctx .macro_ns (
1674
+ fn .var .ns if fn .var .ns is not ctx .current_ns else None
1675
+ ):
1676
+ expanded_ast = _analyze_form (ctx , expanded )
1643
1677
1644
1678
# Verify that macroexpanded code also does not have any
1645
1679
# non-tail recur forms
@@ -2085,34 +2119,60 @@ def _resolve_nested_symbol(ctx: AnalyzerContext, form: sym.Symbol) -> HostField:
2085
2119
)
2086
2120
2087
2121
2088
- def __resolve_namespaced_symbol ( # pylint: disable=too-many-branches
2089
- ctx : AnalyzerContext , form : sym .Symbol
2090
- ) -> Union [Const , HostField , MaybeClass , MaybeHostForm , VarRef ]:
2091
- """Resolve a namespaced symbol into a Python name or Basilisp Var."""
2122
+ def __fuzzy_resolve_namespace_reference (
2123
+ ctx : AnalyzerContext , which_ns : runtime .Namespace , form : sym .Symbol
2124
+ ) -> Optional [VarRef ]:
2125
+ """Resolve a symbol within `which_ns` based on any namespaces required or otherwise
2126
+ referenced within `which_ns` (e.g. by a :refer).
2127
+
2128
+ When a required or resolved symbol is read by the reader in the context of a syntax
2129
+ quote, the reader will fully resolve the symbol, so a symbol like `set/union` would be
2130
+ expanded to `basilisp.set/union`. However, the namespace still does not maintain a
2131
+ direct mapping of the symbol `basilisp.set` to the namespace it names, since the
2132
+ namespace was required as `[basilisp.set :as set]`.
2133
+
2134
+ During macroexpansion, the Analyzer needs to resolve these transitive requirements,
2135
+ so we 'fuzzy' resolve against any namespaces known to the current macro namespace."""
2092
2136
assert form .ns is not None
2137
+ ns_name = form .ns
2138
+
2139
+ def resolve_ns_reference (
2140
+ ns_map : Mapping [str , runtime .Namespace ]
2141
+ ) -> Optional [VarRef ]:
2142
+ match : Optional [runtime .Namespace ] = ns_map .get (ns_name )
2143
+ if match is not None :
2144
+ v = match .find (sym .symbol (form .name ))
2145
+ if v is not None :
2146
+ return VarRef (form = form , var = v , env = ctx .get_node_env ())
2147
+ return None
2093
2148
2094
- if form .ns == ctx .current_ns .name :
2095
- v = ctx .current_ns .find (sym .symbol (form .name ))
2096
- if v is not None :
2097
- return VarRef (form = form , var = v , env = ctx .get_node_env ())
2098
- elif form .ns == _BUILTINS_NS :
2099
- class_ = munge (form .name , allow_builtins = True )
2100
- target = getattr (builtins , class_ , None )
2101
- if target is None :
2102
- raise AnalyzerException (
2103
- f"cannot resolve builtin function '{ class_ } '" , form = form
2104
- )
2105
- return MaybeClass (
2106
- form = form , class_ = class_ , target = target , env = ctx .get_node_env ()
2107
- )
2149
+ # Try to match a required namespace
2150
+ required_namespaces = {ns .name : ns for ns in which_ns .aliases .values ()}
2151
+ match = resolve_ns_reference (required_namespaces )
2152
+ if match is not None :
2153
+ return match
2108
2154
2109
- if "." in form .name and form .name != _DOUBLE_DOT_MACRO_NAME :
2110
- raise AnalyzerException (
2111
- "symbol names may not contain the '.' operator" , form = form
2112
- )
2155
+ # Try to match a referred namespace
2156
+ referred_namespaces = {
2157
+ ns .name : ns for ns in {var .ns for var in which_ns .refers .values ()}
2158
+ }
2159
+ return resolve_ns_reference (referred_namespaces )
2160
+
2161
+
2162
+ def __resolve_namespaced_symbol_in_ns ( # pylint: disable=too-many-branches
2163
+ ctx : AnalyzerContext ,
2164
+ which_ns : runtime .Namespace ,
2165
+ form : sym .Symbol ,
2166
+ allow_fuzzy_macroexpansion_matching : bool = False ,
2167
+ ) -> Optional [Union [MaybeHostForm , VarRef ]]:
2168
+ """Resolve the symbol `form` in the context of the Namespace `which_ns`. If
2169
+ `allow_fuzzy_macroexpansion_matching` is True and no match is made on existing
2170
+ imports, import aliases, or namespace aliases, then attempt to match the
2171
+ namespace portion"""
2172
+ assert form .ns is not None
2113
2173
2114
2174
ns_sym = sym .symbol (form .ns )
2115
- if ns_sym in ctx . current_ns . imports or ns_sym in ctx . current_ns .import_aliases :
2175
+ if ns_sym in which_ns . imports or ns_sym in which_ns .import_aliases :
2116
2176
# We still import Basilisp code, so we'll want to make sure
2117
2177
# that the symbol isn't referring to a Basilisp Var first
2118
2178
v = Var .find (form )
@@ -2123,8 +2183,8 @@ def __resolve_namespaced_symbol( # pylint: disable=too-many-branches
2123
2183
# We don't need this for actually generating the link later, but
2124
2184
# we _do_ need it for fetching a reference to the module to check
2125
2185
# for membership.
2126
- if ns_sym in ctx . current_ns .import_aliases :
2127
- ns = ctx . current_ns .import_aliases [ns_sym ]
2186
+ if ns_sym in which_ns .import_aliases :
2187
+ ns = which_ns .import_aliases [ns_sym ]
2128
2188
assert ns is not None
2129
2189
ns_name = ns .name
2130
2190
else :
@@ -2161,16 +2221,58 @@ def __resolve_namespaced_symbol( # pylint: disable=too-many-branches
2161
2221
target = vars (ns_module )[safe_name ],
2162
2222
env = ctx .get_node_env (),
2163
2223
)
2164
- elif ns_sym in ctx . current_ns .aliases :
2165
- aliased_ns : runtime .Namespace = ctx . current_ns .aliases [ns_sym ]
2224
+ elif ns_sym in which_ns .aliases :
2225
+ aliased_ns : runtime .Namespace = which_ns .aliases [ns_sym ]
2166
2226
v = Var .find (sym .symbol (form .name , ns = aliased_ns .name ))
2167
2227
if v is None :
2168
2228
raise AnalyzerException (
2169
2229
f"unable to resolve symbol '{ sym .symbol (form .name , ns_sym .name )} ' in this context" ,
2170
2230
form = form ,
2171
2231
)
2172
2232
return VarRef (form = form , var = v , env = ctx .get_node_env ())
2173
- elif "." in form .ns :
2233
+ elif allow_fuzzy_macroexpansion_matching :
2234
+ return __fuzzy_resolve_namespace_reference (ctx , which_ns , form )
2235
+
2236
+ return None
2237
+
2238
+
2239
+ def __resolve_namespaced_symbol ( # pylint: disable=too-many-branches
2240
+ ctx : AnalyzerContext , form : sym .Symbol
2241
+ ) -> Union [Const , HostField , MaybeClass , MaybeHostForm , VarRef ]:
2242
+ """Resolve a namespaced symbol into a Python name or Basilisp Var."""
2243
+ assert form .ns is not None
2244
+
2245
+ if form .ns == ctx .current_ns .name :
2246
+ v = ctx .current_ns .find (sym .symbol (form .name ))
2247
+ if v is not None :
2248
+ return VarRef (form = form , var = v , env = ctx .get_node_env ())
2249
+ elif form .ns == _BUILTINS_NS :
2250
+ class_ = munge (form .name , allow_builtins = True )
2251
+ target = getattr (builtins , class_ , None )
2252
+ if target is None :
2253
+ raise AnalyzerException (
2254
+ f"cannot resolve builtin function '{ class_ } '" , form = form
2255
+ )
2256
+ return MaybeClass (
2257
+ form = form , class_ = class_ , target = target , env = ctx .get_node_env ()
2258
+ )
2259
+
2260
+ if "." in form .name and form .name != _DOUBLE_DOT_MACRO_NAME :
2261
+ raise AnalyzerException (
2262
+ "symbol names may not contain the '.' operator" , form = form
2263
+ )
2264
+
2265
+ resolved = __resolve_namespaced_symbol_in_ns (ctx , ctx .current_ns , form )
2266
+ if resolved is not None :
2267
+ return resolved
2268
+ elif ctx .current_macro_ns is not None :
2269
+ resolved = __resolve_namespaced_symbol_in_ns (
2270
+ ctx , ctx .current_macro_ns , form , allow_fuzzy_macroexpansion_matching = True
2271
+ )
2272
+ if resolved is not None :
2273
+ return resolved
2274
+
2275
+ if "." in form .ns :
2174
2276
return _resolve_nested_symbol (ctx , form )
2175
2277
elif ctx .should_allow_unresolved_symbols :
2176
2278
return _const_node (ctx , form )
@@ -2192,6 +2294,12 @@ def __resolve_bare_symbol(
2192
2294
if v is not None :
2193
2295
return VarRef (form = form , var = v , env = ctx .get_node_env ())
2194
2296
2297
+ # Look up the symbol in the current macro namespace, if one
2298
+ if ctx .current_macro_ns is not None :
2299
+ v = ctx .current_macro_ns .find (form )
2300
+ if v is not None :
2301
+ return VarRef (form = form , var = v , env = ctx .get_node_env ())
2302
+
2195
2303
if "." in form .name :
2196
2304
raise AnalyzerException (
2197
2305
"symbol names may not contain the '.' operator" , form = form
0 commit comments