48
48
from basilisp .lang .util import genname , munge
49
49
from basilisp .util import Maybe , partition
50
50
51
+ # Compiler logging
51
52
logger = logging .getLogger (__name__ )
52
53
53
54
# Compiler options
54
55
USE_VAR_INDIRECTION = "use_var_indirection"
55
56
WARN_ON_SHADOWED_NAME = "warn_on_shadowed_name"
56
57
WARN_ON_SHADOWED_VAR = "warn_on_shadowed_var"
58
+ WARN_ON_UNUSED_NAMES = "warn_on_unused_names"
57
59
WARN_ON_VAR_INDIRECTION = "warn_on_var_indirection"
58
60
61
+ # String constants used in generating code
59
62
_BUILTINS_NS = "builtins"
60
63
_CORE_NS = "basilisp.core"
61
64
_DEFAULT_FN = "__lisp_expr__"
69
72
_NS_VAR = "__NS"
70
73
_LISP_NS_VAR = "*ns*"
71
74
75
+ # Special form symbols
72
76
_AMPERSAND = sym .symbol ("&" )
73
77
_CATCH = sym .symbol ("catch" )
74
78
_DEF = sym .symbol ("def" )
104
108
_UNQUOTE = sym .symbol ("unquote" , _CORE_NS )
105
109
_UNQUOTE_SPLICING = sym .symbol ("unquote-splicing" , _CORE_NS )
106
110
111
+ # Symbols to be ignored for unused symbol warnings
112
+ _IGNORED_SYM = sym .symbol ("_" )
113
+ _MACRO_ENV_SYM = sym .symbol ("&env" )
114
+ _MACRO_FORM_SYM = sym .symbol ("&form" )
115
+ _NO_WARN_UNUSED_SYMS = lset .s (_IGNORED_SYM , _MACRO_ENV_SYM , _MACRO_FORM_SYM )
116
+
117
+ # Symbol table contexts
107
118
_SYM_CTX_LOCAL_STARRED = kw .keyword (
108
119
"local-starred" , ns = "basilisp.lang.compiler.var-context"
109
120
)
110
121
_SYM_CTX_LOCAL = kw .keyword ("local" , ns = "basilisp.lang.compiler.var-context" )
122
+ _SYM_CTX_RECUR = kw .keyword ("recur" , ns = "basilisp.lang.compiler.var-context" )
123
+
111
124
112
- SymbolTableEntry = Tuple [str , kw .Keyword , sym .Symbol ]
125
+ class SymbolTableEntry (NamedTuple ):
126
+ munged : str
127
+ context : kw .Keyword
128
+ symbol : sym .Symbol
129
+ used : bool = False
130
+ warn_if_unused : bool = True
113
131
114
132
115
133
class SymbolTable :
116
- CONTEXTS = frozenset ([_SYM_CTX_LOCAL , _SYM_CTX_LOCAL_STARRED ])
134
+ CONTEXTS = frozenset ([_SYM_CTX_LOCAL , _SYM_CTX_LOCAL_STARRED , _SYM_CTX_RECUR ])
117
135
118
136
__slots__ = ("_name" , "_parent" , "_table" , "_children" )
119
137
@@ -139,10 +157,18 @@ def __repr__(self):
139
157
f"table={ repr (self ._table )} , children={ len (self ._children )} )"
140
158
)
141
159
142
- def new_symbol (self , s : sym .Symbol , munged : str , ctx : kw .Keyword ) -> "SymbolTable" :
143
- if ctx not in SymbolTable .CONTEXTS :
144
- raise TypeError (f"Context { ctx } not a valid Symbol Context" )
145
- self ._table [s ] = (munged , ctx , s )
160
+ def new_symbol (
161
+ self , s : sym .Symbol , munged : str , ctx : kw .Keyword , warn_if_unused : bool = True
162
+ ) -> "SymbolTable" :
163
+ assert ctx in SymbolTable .CONTEXTS , f"Symbol context { ctx } must be in CONTEXTS"
164
+ if s in self ._table :
165
+ self ._table [s ] = self ._table [s ]._replace (
166
+ munged = munged , context = ctx , symbol = s , warn_if_unused = warn_if_unused
167
+ )
168
+ else :
169
+ self ._table [s ] = SymbolTableEntry (
170
+ munged , ctx , s , warn_if_unused = warn_if_unused
171
+ )
146
172
return self
147
173
148
174
def find_symbol (self , s : sym .Symbol ) -> Optional [SymbolTableEntry ]:
@@ -152,6 +178,51 @@ def find_symbol(self, s: sym.Symbol) -> Optional[SymbolTableEntry]:
152
178
return None
153
179
return self ._parent .find_symbol (s )
154
180
181
+ def mark_used (self , s : sym .Symbol ) -> None :
182
+ """Mark the symbol s used in the current table or the first ancestor table
183
+ which contains the symbol."""
184
+ if s in self ._table :
185
+ old : SymbolTableEntry = self ._table [s ]
186
+ if old .used :
187
+ return
188
+ self ._table [s ] = old ._replace (used = True )
189
+ elif self ._parent is not None :
190
+ self ._parent .mark_used (s )
191
+ else :
192
+ assert False , f"Symbol { s } not defined in any symbol table"
193
+
194
+ def _warn_unused_names (self ):
195
+ """Log a warning message for locally bound names whose values are not used
196
+ by the time the symbol table frame is being popped off the stack.
197
+
198
+ The symbol table contains locally-bound symbols, recur point symbols, and
199
+ symbols bound to var-args in generated Python functions. Only the locally-
200
+ bound symbols are eligible for an unused warning, since it is not common
201
+ that recur points will be used and user code is not permitted to directly
202
+ access the var-args symbol (the compiler inserts an intermediate symbol
203
+ which user code uses).
204
+
205
+ Warnings will not be issued for symbols named '_', '&form', and '&env'. The
206
+ latter symbols appear in macros and a great many macros will never use them."""
207
+ assert logger .isEnabledFor (
208
+ logging .WARNING
209
+ ), "Only warn when logger is configured for WARNING level"
210
+ ns = runtime .get_current_ns ()
211
+ for _ , entry in self ._table .items ():
212
+ if entry .context != _SYM_CTX_LOCAL :
213
+ continue
214
+ if entry .symbol in _NO_WARN_UNUSED_SYMS :
215
+ continue
216
+ if entry .warn_if_unused and not entry .used :
217
+ code_loc = (
218
+ Maybe (entry .symbol .meta )
219
+ .map (lambda m : f": { m .entry (reader .READER_LINE_KW )} " )
220
+ .or_else_get ("" )
221
+ )
222
+ logger .warning (
223
+ f"symbol '{ entry .symbol } ' defined but not used ({ ns } { code_loc } )"
224
+ )
225
+
155
226
def append_frame (self , name : str , parent : "SymbolTable" = None ) -> "SymbolTable" :
156
227
new_frame = SymbolTable (name , parent = parent )
157
228
self ._children [name ] = new_frame
@@ -161,9 +232,14 @@ def pop_frame(self, name: str) -> None:
161
232
del self ._children [name ]
162
233
163
234
@contextlib .contextmanager
164
- def new_frame (self , name ):
235
+ def new_frame (self , name , warn_on_unused_names ):
236
+ """Context manager for creating a new stack frame. If warn_on_unused_names is
237
+ True and the logger is enabled for WARNING, call _warn_unused_names() on the
238
+ child SymbolTable before it is popped."""
165
239
new_frame = self .append_frame (name , parent = self )
166
240
yield new_frame
241
+ if warn_on_unused_names and logger .isEnabledFor (logging .WARNING ):
242
+ new_frame ._warn_unused_names ()
167
243
self .pop_frame (name )
168
244
169
245
@@ -215,6 +291,11 @@ def warn_on_shadowed_var(self) -> bool:
215
291
WARN_ON_SHADOWED_VAR , False
216
292
)
217
293
294
+ @property
295
+ def warn_on_unused_names (self ) -> bool :
296
+ """If True, warn when local names are unused."""
297
+ return self ._opts .entry (WARN_ON_UNUSED_NAMES , True )
298
+
218
299
@property
219
300
def warn_on_var_indirection (self ) -> bool :
220
301
"""If True, warn when a Var reference cannot be direct linked (iff
@@ -260,7 +341,7 @@ def symbol_table(self) -> SymbolTable:
260
341
@contextlib .contextmanager
261
342
def new_symbol_table (self , name ):
262
343
old_st = self .symbol_table
263
- with old_st .new_frame (name ) as st :
344
+ with old_st .new_frame (name , self . warn_on_unused_names ) as st :
264
345
self ._st .append (st )
265
346
yield st
266
347
self ._st .pop ()
@@ -503,6 +584,7 @@ def _meta_kwargs_ast( # pylint:disable=inconsistent-return-statements
503
584
_SYM_DYNAMIC_META_KEY = kw .keyword ("dynamic" )
504
585
_SYM_MACRO_META_KEY = kw .keyword ("macro" )
505
586
_SYM_NO_WARN_ON_REDEF_META_KEY = kw .keyword ("no-warn-on-redef" )
587
+ _SYM_NO_WARN_WHEN_UNUSED_META_KEY = kw .keyword ("no-warn-when-unused" )
506
588
_SYM_REDEF_META_KEY = kw .keyword ("redef" )
507
589
508
590
@@ -542,6 +624,7 @@ def _new_symbol( # pylint: disable=too-many-arguments
542
624
st : Optional [SymbolTable ] = None ,
543
625
warn_on_shadowed_name : bool = True ,
544
626
warn_on_shadowed_var : bool = True ,
627
+ warn_if_unused : bool = True ,
545
628
):
546
629
"""Add a new symbol to the symbol table.
547
630
@@ -566,7 +649,9 @@ def _new_symbol( # pylint: disable=too-many-arguments
566
649
if (warn_on_shadowed_name or warn_on_shadowed_var ) and ctx .warn_on_shadowed_var :
567
650
if ctx .current_ns .find (s ) is not None :
568
651
logger .warning (f"name '{ s } ' shadows def'ed Var from outer scope" )
569
- st .new_symbol (s , munged , sym_ctx )
652
+ if s .meta is not None and s .meta .entry (_SYM_NO_WARN_WHEN_UNUSED_META_KEY , None ):
653
+ warn_if_unused = False
654
+ st .new_symbol (s , munged , sym_ctx , warn_if_unused = warn_if_unused )
570
655
571
656
572
657
def _def_ast (ctx : CompilerContext , form : llist .List ) -> ASTStream :
@@ -928,7 +1013,7 @@ def _single_arity_fn_ast(
928
1013
with ctx .new_symbol_table (py_fn_name ), ctx .new_recur_point (py_fn_name , fndef .first ):
929
1014
# Allow named anonymous functions to recursively call themselves
930
1015
if name is not None :
931
- _new_symbol (ctx , name , py_fn_name , _SYM_CTX_LOCAL )
1016
+ _new_symbol (ctx , name , py_fn_name , _SYM_CTX_RECUR , warn_if_unused = False )
932
1017
933
1018
args , body , vargs = _fn_args_body (ctx , fndef .first , fndef .rest )
934
1019
@@ -997,7 +1082,7 @@ def __f_68(*multi_arity_args):
997
1082
with ctx .new_recur_point (py_fn_name , arity .first ):
998
1083
# Allow named anonymous functions to recursively call themselves
999
1084
if name is not None :
1000
- _new_symbol (ctx , name , py_fn_name , _SYM_CTX_LOCAL )
1085
+ _new_symbol (ctx , name , py_fn_name , _SYM_CTX_RECUR , warn_if_unused = False )
1001
1086
1002
1087
has_rest = any ([has_rest , is_rest ])
1003
1088
arity_name = f"{ py_fn_name } __arity{ '_rest' if is_rest else arg_count } "
@@ -1352,17 +1437,17 @@ def let_32(a_33, b_34, c_35):
1352
1437
1353
1438
# Generate a function to hold the body of the let expression
1354
1439
letname = genname ("let" )
1355
- with ctx . new_symbol_table ( letname ):
1356
- # Suppress shadowing warnings below since the shadow warnings will be
1357
- # emitted by calling _new_symbol in the loop above
1358
- args , body , vargs = _fn_args_body (
1359
- ctx ,
1360
- vec .vector (arg_syms .keys ()),
1361
- runtime .nthrest (form , 2 ),
1362
- warn_on_shadowed_var = False ,
1363
- warn_on_shadowed_name = False ,
1364
- )
1365
- let_fn_body .append (_expressionize (body , letname , args = args , vargs = vargs ))
1440
+
1441
+ # Suppress shadowing warnings below since the shadow warnings will be
1442
+ # emitted by calling _new_symbol in the loop above
1443
+ args , body , vargs = _fn_args_body (
1444
+ ctx ,
1445
+ vec .vector (arg_syms .keys ()),
1446
+ runtime .nthrest (form , 2 ),
1447
+ warn_on_shadowed_var = False ,
1448
+ warn_on_shadowed_name = False ,
1449
+ )
1450
+ let_fn_body .append (_expressionize (body , letname , args = args , vargs = vargs ))
1366
1451
1367
1452
# Generate local variable assignments for processing let bindings
1368
1453
var_names = seq (var_names ).map (lambda n : ast .Name (id = n , ctx = ast .Store ()))
@@ -1960,18 +2045,19 @@ def _sym_ast(ctx: CompilerContext, form: sym.Symbol) -> ASTStream:
1960
2045
return
1961
2046
1962
2047
# Look up local symbols (function parameters, let bindings, etc.)
1963
- st = ctx .symbol_table
1964
- st_sym = st .find_symbol (form )
1965
-
1966
- if st_sym is not None :
1967
- munged , sym_ctx , _ = st_sym
1968
- assert munged is not None , f"Lisp symbol '{ form } ' not found in symbol table"
1969
-
1970
- if sym_ctx == _SYM_CTX_LOCAL :
1971
- yield _node (ast .Name (id = munged , ctx = ast .Load ()))
2048
+ sym_entry = ctx .symbol_table .find_symbol (form )
2049
+ if sym_entry is not None :
2050
+ assert (
2051
+ sym_entry .munged is not None
2052
+ ), f"Lisp symbol '{ form } ' not found in symbol table"
2053
+ assert (
2054
+ sym_entry .context != _SYM_CTX_LOCAL_STARRED
2055
+ ), "Direct access to varargs forbidden"
2056
+
2057
+ if sym_entry .context in {_SYM_CTX_LOCAL , _SYM_CTX_RECUR }:
2058
+ ctx .symbol_table .mark_used (form )
2059
+ yield _node (ast .Name (id = sym_entry .munged , ctx = ast .Load ()))
1972
2060
return
1973
- elif sym_ctx == _SYM_CTX_LOCAL_STARRED :
1974
- raise CompilerException ("Direct access to varargs forbidden" )
1975
2061
1976
2062
# Resolve def'ed symbols, namespace aliases, imports, etc.
1977
2063
resolved = _resolve_sym (ctx , form )
0 commit comments