• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

basilisp-lang / basilisp / 10894258691

17 Sep 2024 12:28AM UTC coverage: 98.886% (-0.007%) from 98.893%
10894258691

Pull #1050

github

web-flow
Merge 90e9187ce into 5ab9c8417
Pull Request #1050: Emit warnings when requiring or importing with an already used alias

1878 of 1885 branches covered (99.63%)

Branch coverage included in aggregate %.

27 of 28 new or added lines in 5 files covered. (96.43%)

29 existing lines in 2 files now uncovered.

8595 of 8706 relevant lines covered (98.73%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.75
/src/basilisp/lang/compiler/analyzer.py
1
# pylint: disable=too-many-branches,too-many-lines,too-many-return-statements
2
import builtins
1✔
3
import collections
1✔
4
import contextlib
1✔
5
import functools
1✔
6
import inspect
1✔
7
import logging
1✔
8
import platform
1✔
9
import re
1✔
10
import sys
1✔
11
import uuid
1✔
12
from datetime import datetime
1✔
13
from decimal import Decimal
1✔
14
from fractions import Fraction
1✔
15
from functools import partial, wraps
1✔
16
from typing import (
1✔
17
    Any,
18
    Callable,
19
    Collection,
20
    Deque,
21
    FrozenSet,
22
    Iterable,
23
    List,
24
    Mapping,
25
    MutableMapping,
26
    MutableSet,
27
    Optional,
28
    Pattern,
29
    Set,
30
    Tuple,
31
    Type,
32
    TypeVar,
33
    Union,
34
    cast,
35
)
36

37
import attr
1✔
38

39
from basilisp.lang import keyword as kw
1✔
40
from basilisp.lang import list as llist
1✔
41
from basilisp.lang import map as lmap
1✔
42
from basilisp.lang import queue as lqueue
1✔
43
from basilisp.lang import reader as reader
1✔
44
from basilisp.lang import runtime as runtime
1✔
45
from basilisp.lang import set as lset
1✔
46
from basilisp.lang import symbol as sym
1✔
47
from basilisp.lang import vector as vec
1✔
48
from basilisp.lang.compiler.constants import (
1✔
49
    AMPERSAND,
50
    ARGLISTS_KW,
51
    COL_KW,
52
    DEFAULT_COMPILER_FILE_PATH,
53
    DOC_KW,
54
    END_COL_KW,
55
    END_LINE_KW,
56
    FILE_KW,
57
    LINE_KW,
58
    NAME_KW,
59
    NS_KW,
60
    REST_KW,
61
    SYM_ABSTRACT_MEMBERS_META_KEY,
62
    SYM_ABSTRACT_META_KEY,
63
    SYM_ASYNC_META_KEY,
64
    SYM_CLASSMETHOD_META_KEY,
65
    SYM_DEFAULT_META_KEY,
66
    SYM_DYNAMIC_META_KEY,
67
    SYM_INLINE_META_KW,
68
    SYM_KWARGS_META_KEY,
69
    SYM_MACRO_META_KEY,
70
    SYM_MUTABLE_META_KEY,
71
    SYM_NO_INLINE_META_KEY,
72
    SYM_NO_WARN_ON_REDEF_META_KEY,
73
    SYM_NO_WARN_ON_SHADOW_META_KEY,
74
    SYM_NO_WARN_WHEN_UNUSED_META_KEY,
75
    SYM_PRIVATE_META_KEY,
76
    SYM_PROPERTY_META_KEY,
77
    SYM_REDEF_META_KEY,
78
    SYM_STATICMETHOD_META_KEY,
79
    SYM_TAG_META_KEY,
80
    SYM_USE_VAR_INDIRECTION_KEY,
81
    VAR_IS_PROTOCOL_META_KEY,
82
    SpecialForm,
83
)
84
from basilisp.lang.compiler.exception import CompilerException, CompilerPhase
1✔
85
from basilisp.lang.compiler.nodes import (
1✔
86
    Assignable,
87
    Await,
88
    Binding,
89
    Catch,
90
    Const,
91
    ConstType,
92
    Def,
93
    DefType,
94
    DefTypeBase,
95
    DefTypeClassMethod,
96
    DefTypeMember,
97
    DefTypeMethod,
98
    DefTypeMethodArity,
99
    DefTypeProperty,
100
    DefTypePythonMember,
101
    DefTypeStaticMethod,
102
    Do,
103
    Fn,
104
    FnArity,
105
    FunctionContext,
106
    HostCall,
107
    HostField,
108
    If,
109
    Import,
110
    ImportAlias,
111
    Invoke,
112
    KeywordArgs,
113
    KeywordArgSupport,
114
    Let,
115
    LetFn,
116
    Local,
117
    LocalType,
118
    Loop,
119
)
120
from basilisp.lang.compiler.nodes import Map as MapNode
1✔
121
from basilisp.lang.compiler.nodes import (
1✔
122
    MaybeClass,
123
    MaybeHostForm,
124
    Node,
125
    NodeEnv,
126
    NodeOp,
127
    NodeSyntacticPosition,
128
    PyDict,
129
    PyList,
130
    PySet,
131
    PyTuple,
132
)
133
from basilisp.lang.compiler.nodes import Queue as QueueNode
1✔
134
from basilisp.lang.compiler.nodes import Quote, Recur, Reify, Require, RequireAlias
1✔
135
from basilisp.lang.compiler.nodes import Set as SetNode
1✔
136
from basilisp.lang.compiler.nodes import SetBang, SpecialFormNode, Throw, Try, VarRef
1✔
137
from basilisp.lang.compiler.nodes import Vector as VectorNode
1✔
138
from basilisp.lang.compiler.nodes import (
1✔
139
    WithMeta,
140
    Yield,
141
    deftype_or_reify_python_member_names,
142
)
143
from basilisp.lang.interfaces import IMeta, INamed, IRecord, ISeq, IType, IWithMeta
1✔
144
from basilisp.lang.runtime import Var
1✔
145
from basilisp.lang.typing import CompilerOpts, LispForm, ReaderForm
1✔
146
from basilisp.lang.util import OBJECT_DUNDER_METHODS, count, genname, is_abstract, munge
1✔
147
from basilisp.logconfig import TRACE
1✔
148
from basilisp.util import Maybe, partition
1✔
149

150
# Analyzer logging
151
logger = logging.getLogger(__name__)
1✔
152

153
# Analyzer options
154
GENERATE_AUTO_INLINES = kw.keyword("generate-auto-inlines")
1✔
155
INLINE_FUNCTIONS = kw.keyword("inline-functions")
1✔
156
WARN_ON_ARITY_MISMATCH = kw.keyword("warn-on-arity-mismatch")
1✔
157
WARN_ON_SHADOWED_NAME = kw.keyword("warn-on-shadowed-name")
1✔
158
WARN_ON_SHADOWED_VAR = kw.keyword("warn-on-shadowed-var")
1✔
159
WARN_ON_UNUSED_NAMES = kw.keyword("warn-on-unused-names")
1✔
160
WARN_ON_NON_DYNAMIC_SET = kw.keyword("warn-on-non-dynamic-set")
1✔
161

162
# Lisp AST node keywords
163
INIT = kw.keyword("init")
1✔
164
META = kw.keyword("meta")
1✔
165
FIXED_ARITY = kw.keyword("fixed-arity")
1✔
166
BODY = kw.keyword("body")
1✔
167
CATCHES = kw.keyword("catches")
1✔
168
FINALLY = kw.keyword("finally")
1✔
169

170
# Constants used in analyzing
171
AS = kw.keyword("as")
1✔
172
IMPLEMENTS = kw.keyword("implements")
1✔
173
INTERFACE = kw.keyword("interface")
1✔
174
STAR_STAR = sym.symbol("**")
1✔
175
_DOUBLE_DOT_MACRO_NAME = ".."
1✔
176
_BUILTINS_NS = "python"
1✔
177

178
# Symbols to be ignored for unused symbol warnings
179
_IGNORED_SYM = sym.symbol("_")
1✔
180
_MACRO_ENV_SYM = sym.symbol("&env")
1✔
181
_MACRO_FORM_SYM = sym.symbol("&form")
1✔
182
_NO_WARN_UNUSED_SYMS = lset.s(_IGNORED_SYM, _MACRO_ENV_SYM, _MACRO_FORM_SYM)
1✔
183

184

185
@attr.define
1✔
186
class RecurPoint:
1✔
187
    loop_id: str
1✔
188
    args: Collection[Binding] = ()
1✔
189

190

191
@attr.frozen
1✔
192
class SymbolTableEntry:
1✔
193
    binding: Binding
1✔
194
    used: bool = False
1✔
195
    warn_if_unused: bool = True
1✔
196

197
    @property
1✔
198
    def symbol(self) -> sym.Symbol:
1✔
199
        return self.binding.form
1✔
200

201
    @property
1✔
202
    def context(self) -> LocalType:
1✔
203
        return self.binding.local
1✔
204

205

206
@attr.define
1✔
207
class SymbolTable:
1✔
208
    name: str
1✔
209
    _is_context_boundary: bool = False
1✔
210
    _parent: Optional["SymbolTable"] = None
1✔
211
    _table: MutableMapping[sym.Symbol, SymbolTableEntry] = attr.ib(factory=dict)
1✔
212

213
    def new_symbol(
1✔
214
        self, s: sym.Symbol, binding: Binding, warn_if_unused: bool = True
215
    ) -> "SymbolTable":
216
        assert s == binding.form, "Binding symbol must match passed symbol"
1✔
217

218
        if s in self._table:
1✔
219
            self._table[s] = attr.evolve(
1✔
220
                self._table[s], binding=binding, warn_if_unused=warn_if_unused
221
            )
222
        else:
223
            self._table[s] = SymbolTableEntry(binding, warn_if_unused=warn_if_unused)
1✔
224
        return self
1✔
225

226
    def find_symbol(self, s: sym.Symbol) -> Optional[SymbolTableEntry]:
1✔
227
        if s in self._table:
1✔
228
            return self._table[s]
1✔
229
        if self._parent is None:
1✔
230
            return None
1✔
231
        return self._parent.find_symbol(s)
1✔
232

233
    def mark_used(self, s: sym.Symbol) -> None:
1✔
234
        """Mark the symbol s used in the current table or the first ancestor table
235
        which contains the symbol."""
236
        if s in self._table:
1✔
237
            old: SymbolTableEntry = self._table[s]
1✔
238
            if old.used:
1✔
239
                return
1✔
240
            self._table[s] = attr.evolve(old, used=True)
1✔
241
        elif self._parent is not None:
1✔
242
            self._parent.mark_used(s)
1✔
243
        else:  # pragma: no cover
244
            assert False, f"Symbol {s} not defined in any symbol table"
245

246
    def _warn_unused_names(self):
1✔
247
        """Log a warning message for locally bound names whose values are not used
248
        by the time the symbol table frame is being popped off the stack.
249

250
        The symbol table contains locally-bound symbols, recur point symbols, and
251
        symbols bound to var-args in generated Python functions. Only the locally-
252
        bound symbols are eligible for an unused warning, since it is not common
253
        that recur points will be used and user code is not permitted to directly
254
        access the var-args symbol (the compiler inserts an intermediate symbol
255
        which user code uses).
256

257
        Warnings will not be issued for symbols named '_', '&form', and '&env'. The
258
        latter symbols appear in macros and a great many macros will never use them."""
259
        assert logger.isEnabledFor(
1✔
260
            logging.WARNING
261
        ), "Only warn when logger is configured for WARNING level"
262
        ns = runtime.get_current_ns()
1✔
263
        for _, entry in self._table.items():
1✔
264
            if entry.symbol.name.startswith("_"):
1✔
265
                continue
1✔
266
            if entry.symbol in _NO_WARN_UNUSED_SYMS:
1✔
267
                continue
1✔
268
            if entry.warn_if_unused and not entry.used:
1✔
269
                code_loc = (
1✔
270
                    Maybe(entry.symbol.meta)
271
                    .map(lambda m: f": {m.val_at(reader.READER_LINE_KW)}")
272
                    .or_else_get("")
273
                )
274
                logger.warning(
1✔
275
                    f"symbol '{entry.symbol}' defined but not used ({ns}{code_loc})"
276
                )
277

278
    @contextlib.contextmanager
1✔
279
    def new_frame(
1✔
280
        self, name: str, is_context_boundary: bool, warn_on_unused_names: bool
281
    ):
282
        """Context manager for creating a new stack frame. If warn_on_unused_names is
283
        True and the logger is enabled for WARNING, call _warn_unused_names() on the
284
        child SymbolTable before it is popped."""
285
        new_frame = SymbolTable(
1✔
286
            name, is_context_boundary=is_context_boundary, parent=self
287
        )
288
        yield new_frame
1✔
289
        if warn_on_unused_names and logger.isEnabledFor(logging.WARNING):
1✔
290
            new_frame._warn_unused_names()
1✔
291

292
    def _as_env_map(self) -> MutableMapping[sym.Symbol, lmap.PersistentMap]:
1✔
293
        locals_ = {} if self._parent is None else self._parent._as_env_map()
1✔
294
        locals_.update({k: v.binding.to_map() for k, v in self._table.items()})
1✔
295
        return locals_
1✔
296

297
    def as_env_map(self) -> lmap.PersistentMap:
1✔
298
        """Return a map of symbols to the local binding objects in the
299
        local symbol table as of this call."""
300
        return lmap.map(self._as_env_map())
1✔
301

302
    @property
1✔
303
    def context_boundary(self) -> "SymbolTable":
1✔
304
        """Return the nearest context boundary parent symbol table to this one. If the
305
        current table is a context boundary, it will be returned directly.
306

307
        Context boundary symbol tables are symbol tables defined at the top level for
308
        major Python execution boundaries, such as modules (namespaces), functions
309
        (sync and async), and methods.
310

311
        Certain symbols (such as imports) are globally available in the execution
312
        context they are defined in once they have been created, context boundary
313
        symbol tables serve as the anchor points where we hoist these global symbols
314
        so they do not go out of scope when the local table frame is popped."""
315
        if self._is_context_boundary:
1✔
316
            return self
1✔
UNCOV
317
        assert (
×
318
            self._parent is not None
319
        ), "Top symbol table must always be a context boundary"
UNCOV
320
        return self._parent.context_boundary
×
321

322

323
class AnalyzerContext:
1✔
324
    __slots__ = (
1✔
325
        "_allow_unresolved_symbols",
326
        "_filename",
327
        "_func_ctx",
328
        "_is_quoted",
329
        "_opts",
330
        "_recur_points",
331
        "_should_macroexpand",
332
        "_st",
333
        "_syntax_pos",
334
    )
335

336
    def __init__(
1✔
337
        self,
338
        filename: Optional[str] = None,
339
        opts: Optional[CompilerOpts] = None,
340
        should_macroexpand: bool = True,
341
        allow_unresolved_symbols: bool = False,
342
    ) -> None:
343
        self._allow_unresolved_symbols = allow_unresolved_symbols
1✔
344
        self._filename = Maybe(filename).or_else_get(DEFAULT_COMPILER_FILE_PATH)
1✔
345
        self._func_ctx: Deque[FunctionContext] = collections.deque([])
1✔
346
        self._is_quoted: Deque[bool] = collections.deque([])
1✔
347
        self._opts = (
1✔
348
            Maybe(opts).map(lmap.map).or_else_get(lmap.PersistentMap.empty())  # type: ignore[arg-type, unused-ignore]
349
        )
350
        self._recur_points: Deque[RecurPoint] = collections.deque([])
1✔
351
        self._should_macroexpand = should_macroexpand
1✔
352
        self._st = collections.deque([SymbolTable("<Top>", is_context_boundary=True)])
1✔
353
        self._syntax_pos = collections.deque([NodeSyntacticPosition.EXPR])
1✔
354

355
    @property
1✔
356
    def current_ns(self) -> runtime.Namespace:
1✔
357
        return runtime.get_current_ns()
1✔
358

359
    @property
1✔
360
    def filename(self) -> str:
1✔
361
        return self._filename
1✔
362

363
    @property
1✔
364
    def should_generate_auto_inlines(self) -> bool:
1✔
365
        """If True, generate inline function defs for functions with boolean `^:inline`
366
        meta keys."""
367
        return self._opts.val_at(GENERATE_AUTO_INLINES, True)
1✔
368

369
    @property
1✔
370
    def should_inline_functions(self) -> bool:
1✔
371
        """If True, function calls may be inlined if an inline def is provided."""
372
        return self._opts.val_at(INLINE_FUNCTIONS, True)
1✔
373

374
    @property
1✔
375
    def warn_on_arity_mismatch(self) -> bool:
1✔
376
        """If True, warn when a Basilisp function invocation is detected with an
377
        unsupported number of arguments."""
378
        return self._opts.val_at(WARN_ON_ARITY_MISMATCH, True)
1✔
379

380
    @property
1✔
381
    def warn_on_unused_names(self) -> bool:
1✔
382
        """If True, warn when local names are unused."""
383
        return self._opts.val_at(WARN_ON_UNUSED_NAMES, True)
1✔
384

385
    @property
1✔
386
    def warn_on_shadowed_name(self) -> bool:
1✔
387
        """If True, warn when a name is shadowed in an inner scope.
388

389
        Implies warn_on_shadowed_var."""
390
        return self._opts.val_at(WARN_ON_SHADOWED_NAME, False)
1✔
391

392
    @property
1✔
393
    def warn_on_shadowed_var(self) -> bool:
1✔
394
        """If True, warn when a def'ed Var name is shadowed in an inner scope.
395

396
        Implied by warn_on_shadowed_name. The value of warn_on_shadowed_name
397
        supersedes the value of this flag."""
398
        return self.warn_on_shadowed_name or self._opts.val_at(
1✔
399
            WARN_ON_SHADOWED_VAR, False
400
        )
401

402
    @property
1✔
403
    def warn_on_non_dynamic_set(self) -> bool:
1✔
404
        """If True, warn when attempting to set! a Var not marked as ^:dynamic."""
405
        return self._opts.val_at(WARN_ON_NON_DYNAMIC_SET, True)
1✔
406

407
    @property
1✔
408
    def is_quoted(self) -> bool:
1✔
409
        try:
1✔
410
            return self._is_quoted[-1] is True
1✔
411
        except IndexError:
1✔
412
            return False
1✔
413

414
    @contextlib.contextmanager
1✔
415
    def quoted(self):
1✔
416
        self._is_quoted.append(True)
1✔
417
        yield
1✔
418
        self._is_quoted.pop()
1✔
419

420
    @property
1✔
421
    def should_allow_unresolved_symbols(self) -> bool:
1✔
422
        """If True, the analyzer will allow unresolved symbols. This is primarily
423
        useful for contexts by the `macroexpand` and `macroexpand-1` core
424
        functions. This would not be a good setting to use for normal compiler
425
        scenarios."""
426
        return self._allow_unresolved_symbols
1✔
427

428
    @property
1✔
429
    def should_macroexpand(self) -> bool:
1✔
430
        """Return True if macros should be expanded."""
431
        return self._should_macroexpand
1✔
432

433
    @property
1✔
434
    def func_ctx(self) -> Optional[FunctionContext]:
1✔
435
        """Return the current function or method context of the current node, if one.
436
        Return None otherwise.
437

438
        It is possible that the current function is defined inside other functions,
439
        so this does not imply anything about the nesting level of the current node."""
440
        try:
1✔
441
            return self._func_ctx[-1]
1✔
442
        except IndexError:
1✔
443
            return None
1✔
444

445
    @property
1✔
446
    def is_async_ctx(self) -> bool:
1✔
447
        """Return True if the current node appears inside of an async function
448
        definition. Return False otherwise.
449

450
        It is possible that the current function is defined inside other functions,
451
        so this does not imply anything about the nesting level of the current node."""
452
        return self.func_ctx == FunctionContext.ASYNC_FUNCTION
1✔
453

454
    @contextlib.contextmanager
1✔
455
    def new_func_ctx(self, context_type: FunctionContext):
1✔
456
        """Context manager which can be used to set a function or method context for
457
        child nodes to examine. A new function context is pushed onto the stack each
458
        time the Analyzer finds a new function or method definition, so there may be
459
        many nested function contexts."""
460
        self._func_ctx.append(context_type)
1✔
461
        yield
1✔
462
        self._func_ctx.pop()
1✔
463

464
    @property
1✔
465
    def recur_point(self) -> Optional[RecurPoint]:
1✔
466
        """Return the current recur point which applies to the current node, if there
467
        is one."""
468
        try:
1✔
469
            return self._recur_points[-1]
1✔
470
        except IndexError:
1✔
471
            return None
1✔
472

473
    @contextlib.contextmanager
1✔
474
    def new_recur_point(self, loop_id: str, args: Collection[Any] = ()):
1✔
475
        """Context manager which can be used to set a recur point for child nodes.
476
        A new recur point is pushed onto the stack each time the Analyzer finds a
477
        form which supports recursion (such as `fn*` or `loop*`), so there may be
478
        many recur points, though only one may be active at any given time for a
479
        node."""
480
        self._recur_points.append(RecurPoint(loop_id, args=args))
1✔
481
        yield
1✔
482
        self._recur_points.pop()
1✔
483

484
    @property
1✔
485
    def symbol_table(self) -> SymbolTable:
1✔
486
        return self._st[-1]
1✔
487

488
    def put_new_symbol(  # pylint: disable=too-many-arguments
1✔
489
        self,
490
        s: sym.Symbol,
491
        binding: Binding,
492
        warn_on_shadowed_name: bool = True,
493
        warn_on_shadowed_var: bool = True,
494
        warn_if_unused: bool = True,
495
        symbol_table: Optional[SymbolTable] = None,
496
    ):
497
        """Add a new symbol to the symbol table.
498

499
        This function allows individual warnings to be disabled for one run
500
        by supplying keyword arguments temporarily disabling those warnings.
501
        In certain cases, we do not want to issue warnings again for a
502
        previously checked case, so this is a simple way of disabling these
503
        warnings for those cases.
504

505
        There are cases where undesired warnings may be triggered non-locally,
506
        so the Python keyword arguments cannot be used to suppress unwanted
507
        warnings. For these cases, symbols may include the `:no-warn-on-shadow`
508
        metadata key to indicate that warnings for shadowing names from outer
509
        scopes should be suppressed. It is not currently possible to suppress
510
        Var shadowing warnings at the symbol level.
511

512
        If WARN_ON_SHADOWED_NAME compiler option is active and the
513
        warn_on_shadowed_name keyword argument is True, then a warning will be
514
        emitted if a local name is shadowed by another local name. Note that
515
        WARN_ON_SHADOWED_NAME implies WARN_ON_SHADOWED_VAR.
516

517
        If WARN_ON_SHADOWED_VAR compiler option is active and the
518
        warn_on_shadowed_var keyword argument is True, then a warning will be
519
        emitted if a named var is shadowed by a local name."""
520
        st = symbol_table or self.symbol_table
1✔
521
        no_warn_on_shadow = (
1✔
522
            Maybe(s.meta)
523
            .map(lambda m: m.val_at(SYM_NO_WARN_ON_SHADOW_META_KEY, False))
524
            .or_else_get(False)
525
        )
526
        if (
1✔
527
            not no_warn_on_shadow
528
            and warn_on_shadowed_name
529
            and self.warn_on_shadowed_name
530
        ):
531
            if st.find_symbol(s) is not None:
1✔
532
                logger.warning(f"name '{s}' shadows name from outer scope")
1✔
533
        if (
1✔
534
            warn_on_shadowed_name or warn_on_shadowed_var
535
        ) and self.warn_on_shadowed_var:
536
            if self.current_ns.find(s) is not None:
1✔
537
                logger.warning(f"name '{s}' shadows def'ed Var from outer scope")
1✔
538
        if s.meta is not None and s.meta.val_at(SYM_NO_WARN_WHEN_UNUSED_META_KEY, None):
1✔
539
            warn_if_unused = False
1✔
540
        st.new_symbol(s, binding, warn_if_unused=warn_if_unused)
1✔
541

542
    @contextlib.contextmanager
1✔
543
    def new_symbol_table(self, name: str, is_context_boundary: bool = False):
1✔
544
        old_st = self.symbol_table
1✔
545
        with old_st.new_frame(
1✔
546
            name,
547
            is_context_boundary,
548
            self.warn_on_unused_names,
549
        ) as st:
550
            self._st.append(st)
1✔
551
            yield st
1✔
552
            self._st.pop()
1✔
553

554
    @contextlib.contextmanager
1✔
555
    def hide_parent_symbol_table(self):
1✔
556
        """Hide the immediate parent symbol table by temporarily popping
557
        it off the stack.
558

559
        Obviously doing this could have serious adverse consequences if another
560
        new symbol table is added to the stack during this operation, so it
561
        should essentially NEVER be used.
562

563
        Right now, it is being used to hide fields and `this` symbol from
564
        static and class methods."""
565
        old_st = self._st.pop()
1✔
566
        try:
1✔
567
            yield self.symbol_table
1✔
568
        finally:
569
            self._st.append(old_st)
1✔
570

571
    @contextlib.contextmanager
1✔
572
    def expr_pos(self):
1✔
573
        """Context manager which indicates to immediate child nodes that they
574
        are in an expression syntactic position."""
575
        self._syntax_pos.append(NodeSyntacticPosition.EXPR)
1✔
576
        try:
1✔
577
            yield
1✔
578
        finally:
579
            self._syntax_pos.pop()
1✔
580

581
    @contextlib.contextmanager
1✔
582
    def stmt_pos(self):
1✔
583
        """Context manager which indicates to immediate child nodes that they
584
        are in a statement syntactic position."""
585
        self._syntax_pos.append(NodeSyntacticPosition.STMT)
1✔
586
        try:
1✔
587
            yield
1✔
588
        finally:
589
            self._syntax_pos.pop()
1✔
590

591
    @contextlib.contextmanager
1✔
592
    def parent_pos(self):
1✔
593
        """Context manager which indicates to immediate child nodes that they
594
        are in an equivalent syntactic position as their parent node.
595

596
        This context manager copies the top position and pushes a new value onto
597
        the stack, so the parent node's syntax position will not be lost."""
598
        self._syntax_pos.append(self.syntax_position)
1✔
599
        try:
1✔
600
            yield
1✔
601
        finally:
602
            self._syntax_pos.pop()
1✔
603

604
    @property
1✔
605
    def syntax_position(self) -> NodeSyntacticPosition:
1✔
606
        """Return the syntax position of the current node as indicated by its
607
        parent node."""
608
        return self._syntax_pos[-1]
1✔
609

610
    def get_node_env(self, pos: Optional[NodeSyntacticPosition] = None) -> NodeEnv:
1✔
611
        """Return the current Node environment.
612

613
        If a synax position is given, it will be included in the environment.
614
        Otherwise, the position will be set to None."""
615
        return NodeEnv(
1✔
616
            ns=self.current_ns, file=self.filename, pos=pos, func_ctx=self.func_ctx
617
        )
618

619
    def AnalyzerException(
1✔
620
        self,
621
        msg: str,
622
        form: Union[LispForm, None, ISeq] = None,
623
        lisp_ast: Optional[Node] = None,
624
    ) -> CompilerException:
625
        """Return a CompilerException annotated with the current filename and
626
        :analyzer compiler phase set. The remaining keyword arguments are passed
627
        directly to the constructor."""
628
        return CompilerException(
1✔
629
            msg,
630
            phase=CompilerPhase.ANALYZING,
631
            filename=self.filename,
632
            form=form,
633
            lisp_ast=lisp_ast,
634
        )
635

636

637
####################
638
# Private Utilities
639
####################
640

641

642
BoolMetaGetter = Callable[[Union[IMeta, Var]], bool]
1✔
643
MetaGetter = Callable[[Union[IMeta, Var]], Any]
1✔
644

645

646
def _bool_meta_getter(meta_kw: kw.Keyword) -> BoolMetaGetter:
1✔
647
    """Return a function which checks an object with metadata for a boolean
648
    value by meta_kw."""
649

650
    def has_meta_prop(o: Union[IMeta, Var]) -> bool:
1✔
651
        return bool(
1✔
652
            Maybe(o.meta).map(lambda m: m.val_at(meta_kw, None)).or_else_get(False)
653
        )
654

655
    return has_meta_prop
1✔
656

657

658
def _meta_getter(meta_kw: kw.Keyword) -> MetaGetter:
1✔
659
    """Return a function which checks an object with metadata for a value by
660
    meta_kw."""
661

662
    def get_meta_prop(o: Union[IMeta, Var]) -> Any:
1✔
663
        return Maybe(o.meta).map(lambda m: m.val_at(meta_kw, None)).value
1✔
664

665
    return get_meta_prop
1✔
666

667

668
_is_artificially_abstract = _bool_meta_getter(SYM_ABSTRACT_META_KEY)
1✔
669
_artificially_abstract_members = _meta_getter(SYM_ABSTRACT_MEMBERS_META_KEY)
1✔
670
_is_async = _bool_meta_getter(SYM_ASYNC_META_KEY)
1✔
671
_is_mutable = _bool_meta_getter(SYM_MUTABLE_META_KEY)
1✔
672
_is_py_classmethod = _bool_meta_getter(SYM_CLASSMETHOD_META_KEY)
1✔
673
_is_py_property = _bool_meta_getter(SYM_PROPERTY_META_KEY)
1✔
674
_is_py_staticmethod = _bool_meta_getter(SYM_STATICMETHOD_META_KEY)
1✔
675
_is_macro = _bool_meta_getter(SYM_MACRO_META_KEY)
1✔
676
_is_no_inline = _bool_meta_getter(SYM_NO_INLINE_META_KEY)
1✔
677
_is_use_var_indirection = _bool_meta_getter(SYM_USE_VAR_INDIRECTION_KEY)
1✔
678
_inline_meta = _meta_getter(SYM_INLINE_META_KW)
1✔
679
_tag_meta = _meta_getter(SYM_TAG_META_KEY)
1✔
680

681

682
T_form = TypeVar("T_form", bound=ReaderForm)
1✔
683
T_node = TypeVar("T_node", bound=Node)
1✔
684
LispAnalyzer = Callable[[T_form, AnalyzerContext], T_node]
1✔
685

686

687
def _loc(form: T_form) -> Optional[Tuple[int, int, int, int]]:
1✔
688
    """Fetch the location of the form in the original filename from the
689
    input form, if it has metadata."""
690
    # Technically, IMeta is sufficient for fetching `form.meta` but the
691
    # reader only applies line and column metadata to IWithMeta instances
692
    if isinstance(form, IWithMeta):
1✔
693
        meta = form.meta
1✔
694
        if meta is not None:
1✔
695
            line = meta.get(reader.READER_LINE_KW)
1✔
696
            col = meta.get(reader.READER_COL_KW)
1✔
697
            end_line = meta.get(reader.READER_END_LINE_KW)
1✔
698
            end_col = meta.get(reader.READER_END_COL_KW)
1✔
699
            if (
1✔
700
                isinstance(line, int)
701
                and isinstance(col, int)
702
                and isinstance(end_line, int)
703
                and isinstance(end_col, int)
704
            ):
705
                return line, col, end_line, end_col
1✔
706
    return None
1✔
707

708

709
def _with_loc(f: LispAnalyzer[T_form, T_node]) -> LispAnalyzer[T_form, T_node]:
1✔
710
    """Attach any available location information from the input form to
711
    the node environment returned from the parsing function."""
712

713
    @wraps(f)
1✔
714
    def _analyze_form(form: T_form, ctx: AnalyzerContext) -> T_node:
1✔
715
        form_loc = _loc(form)
1✔
716
        if form_loc is None:
1✔
717
            return f(form, ctx)
1✔
718
        else:
719
            return cast(T_node, f(form, ctx).fix_missing_locations(form_loc))
1✔
720

721
    return _analyze_form
1✔
722

723

724
def _clean_meta(meta: Optional[lmap.PersistentMap]) -> Optional[lmap.PersistentMap]:
1✔
725
    """Remove reader metadata from the form's meta map."""
726
    if meta is None:
1✔
727
        return None
1✔
728
    else:
729
        new_meta = meta.dissoc(
1✔
730
            reader.READER_LINE_KW,
731
            reader.READER_COL_KW,
732
            reader.READER_END_LINE_KW,
733
            reader.READER_END_COL_KW,
734
        )
735
        return None if len(new_meta) == 0 else new_meta
1✔
736

737

738
def _body_ast(
1✔
739
    form: Union[llist.PersistentList, ISeq], ctx: AnalyzerContext
740
) -> Tuple[Iterable[Node], Node]:
741
    """Analyze the form and produce a body of statement nodes and a single
742
    return expression node.
743

744
    If the body is empty, return a constant node containing nil.
745

746
    If the parent indicates that it is in a statement syntactic position
747
    (and thus that it cannot return a value), the final node will be marked
748
    as a statement (rather than an expression) as well."""
749
    body_list = list(form)
1✔
750
    if body_list:
1✔
751
        *stmt_forms, ret_form = body_list
1✔
752

753
        with ctx.stmt_pos():
1✔
754
            body_stmts = list(map(lambda form: _analyze_form(form, ctx), stmt_forms))
1✔
755

756
        with ctx.parent_pos():
1✔
757
            body_expr = _analyze_form(ret_form, ctx)
1✔
758

759
        body = body_stmts + [body_expr]
1✔
760
    else:
761
        body = []
1✔
762

763
    if body:
1✔
764
        *stmts, ret = body
1✔
765
    else:
766
        stmts, ret = [], _const_node(None, ctx)
1✔
767
    return stmts, ret
1✔
768

769

770
def _call_args_ast(
1✔
771
    form: ISeq, ctx: AnalyzerContext
772
) -> Tuple[Iterable[Node], KeywordArgs]:
773
    """Return a tuple of positional arguments and keyword arguments, splitting at the
774
    keyword argument marker symbol '**'."""
775
    with ctx.expr_pos():
1✔
776
        nmarkers = sum(int(e == STAR_STAR) for e in form)
1✔
777
        if nmarkers > 1:
1✔
778
            raise ctx.AnalyzerException(
1✔
779
                "function and method invocations may have at most 1 keyword argument marker '**'",
780
                form=form,
781
            )
782
        elif nmarkers == 1:
1✔
783
            kwarg_marker = False
1✔
784
            pos, kws = [], []
1✔
785
            for arg in form:
1✔
786
                if arg == STAR_STAR:
1✔
787
                    kwarg_marker = True
1✔
788
                    continue
1✔
789
                if kwarg_marker:
1✔
790
                    kws.append(arg)
1✔
791
                else:
792
                    pos.append(arg)
1✔
793

794
            args = vec.vector(map(lambda form: _analyze_form(form, ctx), pos))
1✔
795
            kw_map = {}
1✔
796
            try:
1✔
797
                for k, v in partition(kws, 2):
1✔
798
                    if isinstance(k, kw.Keyword):
1✔
799
                        munged_k = munge(k.name, allow_builtins=True)
1✔
800
                    elif isinstance(k, str):
1✔
801
                        munged_k = munge(k, allow_builtins=True)
1✔
802
                    else:
803
                        raise ctx.AnalyzerException(
1✔
804
                            f"keys for keyword arguments must be keywords or strings, not '{type(k)}'",
805
                            form=k,
806
                        )
807

808
                    if munged_k in kw_map:
1✔
809
                        raise ctx.AnalyzerException(
1✔
810
                            "duplicate keyword argument key in function or method invocation",
811
                            form=k,
812
                        )
813

814
                    kw_map[munged_k] = _analyze_form(v, ctx)
1✔
815

816
            except ValueError as e:
1✔
817
                raise ctx.AnalyzerException(
1✔
818
                    "keyword arguments must appear in key/value pairs", form=form
819
                ) from e
820
            else:
821
                kwargs = lmap.map(kw_map)
1✔
822
        else:
823
            args = vec.vector(map(lambda form: _analyze_form(form, ctx), form))
1✔
824
            kwargs = lmap.PersistentMap.empty()
1✔
825

826
        return args, kwargs
1✔
827

828

829
def _tag_ast(form: Optional[LispForm], ctx: AnalyzerContext) -> Optional[Node]:
1✔
830
    if form is None:
1✔
831
        return None
1✔
832
    return _analyze_form(form, ctx)
1✔
833

834

835
def _with_meta(gen_node: LispAnalyzer[T_form, T_node]) -> LispAnalyzer[T_form, T_node]:
1✔
836
    """Wraps the node generated by gen_node in a :with-meta AST node if the
837
    original form has meta.
838

839
    :with-meta AST nodes are used for non-quoted collection literals and for
840
    function expressions."""
841

842
    @wraps(gen_node)
1✔
843
    def with_meta(form: T_form, ctx: AnalyzerContext) -> T_node:
1✔
844
        assert not ctx.is_quoted, "with-meta nodes are not used in quoted expressions"
1✔
845

846
        descriptor = gen_node(form, ctx)
1✔
847

848
        if isinstance(form, IMeta):
1✔
849
            assert isinstance(form.meta, (lmap.PersistentMap, type(None)))
1✔
850
            form_meta = _clean_meta(form.meta)
1✔
851
            if form_meta is not None:
1✔
852
                meta_ast = _analyze_form(form_meta, ctx)
1✔
853
                assert isinstance(meta_ast, MapNode) or (
1✔
854
                    isinstance(meta_ast, Const) and meta_ast.type == ConstType.MAP
855
                )
856
                return cast(
1✔
857
                    T_node,
858
                    WithMeta(
859
                        form=cast(LispForm, form),
860
                        meta=meta_ast,
861
                        expr=descriptor,
862
                        env=ctx.get_node_env(pos=ctx.syntax_position),
863
                    ),
864
                )
865

866
        return descriptor
1✔
867

868
    return with_meta
1✔
869

870

871
######################
872
# Analyzer Entrypoint
873
######################
874

875

876
@functools.singledispatch
1✔
877
def _analyze_form(form: Union[ReaderForm, ISeq], ctx: AnalyzerContext):
1✔
UNCOV
878
    raise ctx.AnalyzerException(f"Unexpected form type {type(form)}", form=form)  # type: ignore[arg-type]
×
879

880

881
################
882
# Special Forms
883
################
884

885

886
def _await_ast(form: ISeq, ctx: AnalyzerContext) -> Await:
1✔
887
    assert form.first == SpecialForm.AWAIT
1✔
888

889
    if not ctx.is_async_ctx:
1✔
890
        raise ctx.AnalyzerException(
1✔
891
            "await forms may not appear in non-async context", form=form
892
        )
893

894
    nelems = count(form)
1✔
895
    if nelems != 2:
1✔
896
        raise ctx.AnalyzerException(
1✔
897
            "await forms must contain 2 elements, as in: (await expr)", form=form
898
        )
899

900
    with ctx.expr_pos():
1✔
901
        expr = _analyze_form(runtime.nth(form, 1), ctx)
1✔
902

903
    return Await(
1✔
904
        form=form,
905
        expr=expr,
906
        env=ctx.get_node_env(pos=ctx.syntax_position),
907
    )
908

909

910
def __should_warn_on_redef(
1✔
911
    current_ns: runtime.Namespace,
912
    defsym: sym.Symbol,
913
    def_meta: Optional[lmap.PersistentMap],
914
) -> bool:
915
    """Return True if the compiler should emit a warning about this name being redefined."""
916
    if def_meta is not None and def_meta.val_at(SYM_NO_WARN_ON_REDEF_META_KEY, False):
1✔
917
        return False
1✔
918

919
    if defsym not in current_ns.interns:
1✔
920
        return False
1✔
921

922
    var = current_ns.find(defsym)
1✔
923
    assert var is not None, f"Var {defsym} cannot be none here"
1✔
924

925
    if var.meta is not None and var.meta.val_at(SYM_REDEF_META_KEY):
1✔
926
        return False
1✔
927
    else:
928
        return bool(var.is_bound)
1✔
929

930

931
def _def_ast(  # pylint: disable=too-many-locals,too-many-statements
1✔
932
    form: ISeq, ctx: AnalyzerContext
933
) -> Def:
934
    assert form.first == SpecialForm.DEF
1✔
935

936
    nelems = count(form)
1✔
937
    if nelems not in (2, 3, 4):
1✔
938
        raise ctx.AnalyzerException(
1✔
939
            "def forms must have between 2 and 4 elements, as in: (def name docstring? init?)",
940
            form=form,
941
        )
942

943
    name = runtime.nth(form, 1)
1✔
944
    if not isinstance(name, sym.Symbol):
1✔
945
        raise ctx.AnalyzerException(
1✔
946
            f"def names must be symbols, not {type(name)}", form=name
947
        )
948

949
    tag_ast = _tag_ast(_tag_meta(name), ctx)
1✔
950

951
    init_idx: Optional[int]
952
    children: vec.PersistentVector[kw.Keyword]
953
    if nelems == 2:
1✔
954
        init_idx = None
1✔
955
        doc = None
1✔
956
        children = vec.PersistentVector.empty()
1✔
957
    elif nelems == 3:
1✔
958
        init_idx = 2
1✔
959
        doc = None
1✔
960
        children = vec.v(INIT)
1✔
961
    else:
962
        init_idx = 3
1✔
963
        doc = runtime.nth(form, 2)
1✔
964
        if not isinstance(doc, str):
1✔
965
            raise ctx.AnalyzerException("def docstring must be a string", form=doc)
1✔
966
        children = vec.v(INIT)
1✔
967

968
    # Cache the current namespace
969
    current_ns = ctx.current_ns
1✔
970

971
    # Attach metadata relevant for the current process below.
972
    #
973
    # The reader line/col metadata will be attached to the form itself in the
974
    # happy path case (top-level bare def or top-level def returned from a
975
    # macro). Less commonly, we may have to rely on metadata attached to the
976
    # def symbol if, for example, the def form is returned by a macro wrapped
977
    # in a do or let form. In that case, the macroexpansion process won't have
978
    # any metadata to pass along to the expanded form, but the symbol itself
979
    # is likely to have metadata. In rare cases, we may not be able to get
980
    # any metadata. This may happen if the form and name were both generated
981
    # programmatically.
982
    def_loc = _loc(form) or _loc(name) or (None, None, None, None)
1✔
983
    if def_loc == (None, None, None, None):
1✔
984
        logger.warning(f"def line and column metadata not provided for Var {name}")
1✔
985
    if name.meta is None:
1✔
986
        logger.warning(f"def name symbol has no metadata for Var {name}")
1✔
987
        name = name.with_meta(lmap.EMPTY)
1✔
988
    def_node_env = ctx.get_node_env(pos=ctx.syntax_position)
1✔
989
    def_meta = _clean_meta(
1✔
990
        name.meta.update(  # type: ignore [union-attr]
991
            lmap.map(
992
                {
993
                    COL_KW: def_loc[1],
994
                    END_COL_KW: def_loc[3],
995
                    FILE_KW: def_node_env.file,
996
                    LINE_KW: def_loc[0],
997
                    END_LINE_KW: def_loc[2],
998
                    NAME_KW: name,
999
                    NS_KW: current_ns,
1000
                }
1001
            )
1002
        )
1003
    )
1004
    assert def_meta is not None, "def metadata must be defined at this point"
1✔
1005
    if doc is not None:
1✔
1006
        def_meta = def_meta.assoc(DOC_KW, doc)
1✔
1007

1008
    # Var metadata is set both for the running Basilisp instance
1009
    # and cached as Python bytecode to be reread again. Argument lists
1010
    # are quoted so as not to resolve argument symbols. For the compiled
1011
    # and cached bytecode, this causes no trouble. However, for the case
1012
    # where we directly set the Var meta for the running Basilisp instance
1013
    # this causes problems since we'll end up getting something like
1014
    # `(quote ([] [v]))` rather than simply `([] [v])`.
1015
    arglists_meta = def_meta.val_at(ARGLISTS_KW)  # type: ignore
1✔
1016
    if isinstance(arglists_meta, llist.PersistentList):
1✔
1017
        assert arglists_meta.first == SpecialForm.QUOTE
1✔
1018
        var_meta = def_meta.update(  # type: ignore
1✔
1019
            {ARGLISTS_KW: runtime.nth(arglists_meta, 1)}
1020
        )
1021
    else:
1022
        var_meta = def_meta
1✔
1023

1024
    # Generation fails later if we use the same symbol we received, since
1025
    # its meta may contain values which fail to compile.
1026
    bare_name = sym.symbol(name.name)
1✔
1027

1028
    # Warn if this symbol is potentially being redefined (if the Var was
1029
    # previously bound)
1030
    if __should_warn_on_redef(current_ns, bare_name, def_meta):
1✔
1031
        logger.warning(
1✔
1032
            f"redefining Var '{bare_name}' in namespace {current_ns}:{def_loc[0]}"
1033
        )
1034

1035
    ns_sym = sym.symbol(current_ns.name)
1✔
1036
    var = Var.intern_unbound(
1✔
1037
        ns_sym,
1038
        bare_name,
1039
        dynamic=def_meta.val_at(SYM_DYNAMIC_META_KEY, False),  # type: ignore
1040
        meta=var_meta,
1041
    )
1042

1043
    # Set the value after interning the Var so the symbol is available for
1044
    # recursive definition.
1045
    if init_idx is not None:
1✔
1046
        with ctx.expr_pos():
1✔
1047
            init = _analyze_form(runtime.nth(form, init_idx), ctx)
1✔
1048

1049
        if isinstance(init, Fn):
1✔
1050
            # Attach the automatically generated inline function (if one exists) to the
1051
            # Var and def metadata. We do not need to do this for user-provided inline
1052
            # functions (for which `init.inline_fn` will be None) since those should
1053
            # already be attached to the meta.
1054
            if init.inline_fn is not None:
1✔
1055
                assert isinstance(var.meta.val_at(SYM_INLINE_META_KW), bool), (  # type: ignore[union-attr]
1✔
1056
                    "Cannot have a user-generated inline function and an automatically "
1057
                    "generated inline function"
1058
                )
1059
                var.meta.assoc(SYM_INLINE_META_KW, init.inline_fn)  # type: ignore[union-attr]
1✔
1060
                def_meta = def_meta.assoc(SYM_INLINE_META_KW, init.inline_fn.form)  # type: ignore[union-attr]
1✔
1061

1062
            if tag_ast is not None and any(
1✔
1063
                arity.tag is not None for arity in init.arities
1064
            ):
1065
                raise ctx.AnalyzerException(
1✔
1066
                    "def'ed Var :tag conflicts with defined function :tag",
1067
                    form=form,
1068
                )
1069
    else:
1070
        init = None
1✔
1071

1072
    descriptor = Def(
1✔
1073
        form=form,
1074
        name=bare_name,
1075
        var=var,
1076
        init=init,
1077
        doc=doc,
1078
        children=children,
1079
        env=def_node_env,
1080
        tag=tag_ast,
1081
    )
1082

1083
    # We still have to compile the meta here down to Python source code, so
1084
    # anything which is not constant below needs to be valid Basilisp code
1085
    # at the site it is called.
1086
    #
1087
    # We are roughly generating code like this:
1088
    #
1089
    # (def ^{:col  1
1090
    #        :file "<REPL Input>"
1091
    #        :line 1
1092
    #        :name 'some-name
1093
    #        :ns   ((.- basilisp.lang.runtime/Namespace get) 'basilisp.user)}
1094
    #       some-name
1095
    #       "some value")
1096
    meta_ast = _analyze_form(
1✔
1097
        def_meta.update(  # type: ignore
1098
            {
1099
                NAME_KW: llist.l(SpecialForm.QUOTE, bare_name),
1100
                NS_KW: llist.l(
1101
                    llist.l(
1102
                        SpecialForm.INTEROP_PROP,
1103
                        sym.symbol("Namespace", "basilisp.lang.runtime"),
1104
                        sym.symbol("get"),
1105
                    ),
1106
                    llist.l(SpecialForm.QUOTE, sym.symbol(current_ns.name)),
1107
                ),
1108
            }
1109
        ),
1110
        ctx,
1111
    )
1112

1113
    assert (isinstance(meta_ast, Const) and meta_ast.type == ConstType.MAP) or (
1✔
1114
        isinstance(meta_ast, MapNode)
1115
    )
1116
    existing_children = cast(vec.PersistentVector, descriptor.children)
1✔
1117
    return descriptor.assoc(
1✔
1118
        meta=meta_ast, children=vec.vector(runtime.cons(META, existing_children))
1119
    )
1120

1121

1122
def __deftype_method_param_bindings(
1✔
1123
    params: vec.PersistentVector, ctx: AnalyzerContext, special_form: sym.Symbol
1124
) -> Tuple[bool, int, List[Binding]]:
1125
    """Generate parameter bindings for `deftype*` or `reify*` methods.
1126

1127
    Return a tuple containing a boolean, indicating if the parameter bindings
1128
    contain a variadic binding, an integer indicating the fixed arity of the
1129
    parameter bindings, and the list of parameter bindings.
1130

1131
    Special cases for individual method types must be handled by their
1132
    respective handlers. This method will only produce vanilla ARG type
1133
    bindings."""
1134
    assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
1✔
1135

1136
    has_vargs, vargs_idx = False, 0
1✔
1137
    param_nodes = []
1✔
1138
    for i, s in enumerate(params):
1✔
1139
        if not isinstance(s, sym.Symbol):
1✔
1140
            raise ctx.AnalyzerException(
1✔
1141
                f"{special_form} method parameter name must be a symbol", form=s
1142
            )
1143

1144
        if s == AMPERSAND:
1✔
1145
            has_vargs = True
1✔
1146
            vargs_idx = i
1✔
1147
            break
1✔
1148

1149
        binding = Binding(
1✔
1150
            form=s,
1151
            name=s.name,
1152
            local=LocalType.ARG,
1153
            arg_id=i,
1154
            is_variadic=False,
1155
            env=ctx.get_node_env(),
1156
        )
1157
        param_nodes.append(binding)
1✔
1158
        ctx.put_new_symbol(s, binding)
1✔
1159

1160
    fixed_arity = len(param_nodes)
1✔
1161

1162
    if has_vargs:
1✔
1163
        try:
1✔
1164
            vargs_sym = params[vargs_idx + 1]
1✔
1165

1166
            if not isinstance(vargs_sym, sym.Symbol):
1✔
1167
                raise ctx.AnalyzerException(
1✔
1168
                    f"{special_form} method rest parameter name must be a symbol",
1169
                    form=vargs_sym,
1170
                )
1171

1172
            binding = Binding(
1✔
1173
                form=vargs_sym,
1174
                name=vargs_sym.name,
1175
                local=LocalType.ARG,
1176
                arg_id=vargs_idx + 1,
1177
                is_variadic=True,
1178
                env=ctx.get_node_env(),
1179
            )
1180
            param_nodes.append(binding)
1✔
1181
            ctx.put_new_symbol(vargs_sym, binding)
1✔
1182
        except IndexError:
1✔
1183
            raise ctx.AnalyzerException(
1✔
1184
                "Expected variadic argument name after '&'", form=params
1185
            ) from None
1186

1187
    return has_vargs, fixed_arity, param_nodes
1✔
1188

1189

1190
def __deftype_classmethod(
1✔
1191
    form: Union[llist.PersistentList, ISeq],
1192
    ctx: AnalyzerContext,
1193
    method_name: str,
1194
    args: vec.PersistentVector,
1195
    kwarg_support: Optional[KeywordArgSupport] = None,
1196
) -> DefTypeClassMethod:
1197
    """Emit a node for a :classmethod member of a `deftype*` form."""
1198
    with ctx.hide_parent_symbol_table(), ctx.new_symbol_table(
1✔
1199
        method_name, is_context_boundary=True
1200
    ):
1201
        try:
1✔
1202
            cls_arg = args[0]
1✔
1203
        except IndexError as e:
1✔
1204
            raise ctx.AnalyzerException(
1✔
1205
                "deftype* class method must include 'cls' argument", form=args
1206
            ) from e
1207
        else:
1208
            if not isinstance(cls_arg, sym.Symbol):
1✔
1209
                raise ctx.AnalyzerException(
1✔
1210
                    "deftype* class method 'cls' argument must be a symbol",
1211
                    form=args,
1212
                )
1213
            cls_binding = Binding(
1✔
1214
                form=cls_arg,
1215
                name=cls_arg.name,
1216
                local=LocalType.ARG,
1217
                env=ctx.get_node_env(),
1218
            )
1219
            ctx.put_new_symbol(cls_arg, cls_binding)
1✔
1220

1221
        params = args[1:]
1✔
1222
        has_vargs, fixed_arity, param_nodes = __deftype_method_param_bindings(
1✔
1223
            params, ctx, SpecialForm.DEFTYPE
1224
        )
1225
        with ctx.new_func_ctx(FunctionContext.CLASSMETHOD), ctx.expr_pos():
1✔
1226
            stmts, ret = _body_ast(runtime.nthrest(form, 2), ctx)
1✔
1227
        method = DefTypeClassMethod(
1✔
1228
            form=form,
1229
            name=method_name,
1230
            params=vec.vector(param_nodes),
1231
            fixed_arity=fixed_arity,
1232
            is_variadic=has_vargs,
1233
            kwarg_support=kwarg_support,
1234
            body=Do(
1235
                form=form.rest,
1236
                statements=vec.vector(stmts),
1237
                ret=ret,
1238
                is_body=True,
1239
                # Use the argument vector or first body statement, whichever
1240
                # exists, for metadata.
1241
                env=ctx.get_node_env(),
1242
            ),
1243
            class_local=cls_binding,
1244
            env=ctx.get_node_env(),
1245
        )
1246
        method.visit(partial(_assert_no_recur, ctx))
1✔
1247
        return method
1✔
1248

1249

1250
def __deftype_or_reify_method(  # pylint: disable=too-many-arguments,too-many-locals
1✔
1251
    form: Union[llist.PersistentList, ISeq],
1252
    ctx: AnalyzerContext,
1253
    method_name: str,
1254
    args: vec.PersistentVector,
1255
    special_form: sym.Symbol,
1256
    kwarg_support: Optional[KeywordArgSupport] = None,
1257
) -> DefTypeMethodArity:
1258
    """Emit a node for a method member of a `deftype*` or `reify*` form."""
1259
    assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
1✔
1260

1261
    with ctx.new_symbol_table(method_name, is_context_boundary=True):
1✔
1262
        try:
1✔
1263
            this_arg = args[0]
1✔
1264
        except IndexError as e:
1✔
1265
            raise ctx.AnalyzerException(
1✔
1266
                f"{special_form} method must include 'this' or 'self' argument",
1267
                form=args,
1268
            ) from e
1269
        else:
1270
            if not isinstance(this_arg, sym.Symbol):
1✔
1271
                raise ctx.AnalyzerException(
1✔
1272
                    f"{special_form} method 'this' argument must be a symbol", form=args
1273
                )
1274
            this_binding = Binding(
1✔
1275
                form=this_arg,
1276
                name=this_arg.name,
1277
                local=LocalType.THIS,
1278
                env=ctx.get_node_env(),
1279
            )
1280
            ctx.put_new_symbol(this_arg, this_binding, warn_if_unused=False)
1✔
1281

1282
        params = args[1:]
1✔
1283
        has_vargs, fixed_arity, param_nodes = __deftype_method_param_bindings(
1✔
1284
            params, ctx, special_form
1285
        )
1286

1287
        loop_id = genname(method_name)
1✔
1288
        with ctx.new_recur_point(loop_id, param_nodes):
1✔
1289
            with ctx.new_func_ctx(FunctionContext.METHOD), ctx.expr_pos():
1✔
1290
                stmts, ret = _body_ast(runtime.nthrest(form, 2), ctx)
1✔
1291
            method = DefTypeMethodArity(
1✔
1292
                form=form,
1293
                name=method_name,
1294
                this_local=this_binding,
1295
                params=vec.vector(param_nodes),
1296
                fixed_arity=fixed_arity,
1297
                is_variadic=has_vargs,
1298
                kwarg_support=kwarg_support,
1299
                body=Do(
1300
                    form=form.rest,
1301
                    statements=vec.vector(stmts),
1302
                    ret=ret,
1303
                    is_body=True,
1304
                    # Use the argument vector or first body statement, whichever
1305
                    # exists, for metadata.
1306
                    env=ctx.get_node_env(),
1307
                ),
1308
                loop_id=loop_id,
1309
                env=ctx.get_node_env(),
1310
            )
1311
            method.visit(partial(_assert_recur_is_tail, ctx))
1✔
1312
            return method
1✔
1313

1314

1315
def __deftype_or_reify_property(
1✔
1316
    form: Union[llist.PersistentList, ISeq],
1317
    ctx: AnalyzerContext,
1318
    method_name: str,
1319
    args: vec.PersistentVector,
1320
    special_form: sym.Symbol,
1321
) -> DefTypeProperty:
1322
    """Emit a node for a :property member of a `deftype*` or `reify*` form."""
1323
    assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
1✔
1324

1325
    with ctx.new_symbol_table(method_name, is_context_boundary=True):
1✔
1326
        try:
1✔
1327
            this_arg = args[0]
1✔
1328
        except IndexError as e:
1✔
1329
            raise ctx.AnalyzerException(
1✔
1330
                f"{special_form} property must include 'this' or 'self' argument",
1331
                form=args,
1332
            ) from e
1333
        else:
1334
            if not isinstance(this_arg, sym.Symbol):
1✔
1335
                raise ctx.AnalyzerException(
1✔
1336
                    f"{special_form} property 'this' argument must be a symbol",
1337
                    form=args,
1338
                )
1339
            this_binding = Binding(
1✔
1340
                form=this_arg,
1341
                name=this_arg.name,
1342
                local=LocalType.THIS,
1343
                env=ctx.get_node_env(),
1344
            )
1345
            ctx.put_new_symbol(this_arg, this_binding, warn_if_unused=False)
1✔
1346

1347
        params = args[1:]
1✔
1348
        has_vargs, _, param_nodes = __deftype_method_param_bindings(
1✔
1349
            params, ctx, special_form
1350
        )
1351

1352
        if len(param_nodes) > 0:
1✔
1353
            raise ctx.AnalyzerException(
1✔
1354
                f"{special_form} properties may not specify arguments", form=form
1355
            )
1356

1357
        assert not has_vargs, f"{special_form} properties may not have arguments"
1✔
1358

1359
        with ctx.new_func_ctx(FunctionContext.PROPERTY), ctx.expr_pos():
1✔
1360
            stmts, ret = _body_ast(runtime.nthrest(form, 2), ctx)
1✔
1361
        prop = DefTypeProperty(
1✔
1362
            form=form,
1363
            name=method_name,
1364
            this_local=this_binding,
1365
            params=vec.vector(param_nodes),
1366
            body=Do(
1367
                form=form.rest,
1368
                statements=vec.vector(stmts),
1369
                ret=ret,
1370
                is_body=True,
1371
                # Use the argument vector or first body statement, whichever
1372
                # exists, for metadata.
1373
                env=ctx.get_node_env(),
1374
            ),
1375
            env=ctx.get_node_env(),
1376
        )
1377
        prop.visit(partial(_assert_no_recur, ctx))
1✔
1378
        return prop
1✔
1379

1380

1381
def __deftype_staticmethod(
1✔
1382
    form: Union[llist.PersistentList, ISeq],
1383
    ctx: AnalyzerContext,
1384
    method_name: str,
1385
    args: vec.PersistentVector,
1386
    kwarg_support: Optional[KeywordArgSupport] = None,
1387
) -> DefTypeStaticMethod:
1388
    """Emit a node for a :staticmethod member of a `deftype*` form."""
1389
    with ctx.hide_parent_symbol_table(), ctx.new_symbol_table(
1✔
1390
        method_name, is_context_boundary=True
1391
    ):
1392
        has_vargs, fixed_arity, param_nodes = __deftype_method_param_bindings(
1✔
1393
            args, ctx, SpecialForm.DEFTYPE
1394
        )
1395
        with ctx.new_func_ctx(FunctionContext.STATICMETHOD), ctx.expr_pos():
1✔
1396
            stmts, ret = _body_ast(runtime.nthrest(form, 2), ctx)
1✔
1397
        method = DefTypeStaticMethod(
1✔
1398
            form=form,
1399
            name=method_name,
1400
            params=vec.vector(param_nodes),
1401
            fixed_arity=fixed_arity,
1402
            is_variadic=has_vargs,
1403
            kwarg_support=kwarg_support,
1404
            body=Do(
1405
                form=form.rest,
1406
                statements=vec.vector(stmts),
1407
                ret=ret,
1408
                is_body=True,
1409
                # Use the argument vector or first body statement, whichever
1410
                # exists, for metadata.
1411
                env=ctx.get_node_env(),
1412
            ),
1413
            env=ctx.get_node_env(),
1414
        )
1415
        method.visit(partial(_assert_no_recur, ctx))
1✔
1416
        return method
1✔
1417

1418

1419
def __deftype_or_reify_prop_or_method_arity(
1✔
1420
    form: Union[llist.PersistentList, ISeq],
1421
    ctx: AnalyzerContext,
1422
    special_form: sym.Symbol,
1423
) -> Union[DefTypeMethodArity, DefTypePythonMember]:
1424
    """Emit either a `deftype*` or `reify*` property node or an arity of a `deftype*`
1425
    or `reify*` method.
1426

1427
    Unlike standard `fn*` definitions, multiple arities for a single method are
1428
    not defined within some containing node. As such, we can only emit either a
1429
    full property node (since properties may not be multi-arity) or the single
1430
    arity of a method, classmethod, or staticmethod.
1431

1432
    The type of the member node is determined by the presence or absence of certain
1433
    metadata elements on the input form (or the form's first member, typically a
1434
    symbol naming that member)."""
1435
    assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
1✔
1436

1437
    if not isinstance(form.first, sym.Symbol):
1✔
1438
        raise ctx.AnalyzerException(
1✔
1439
            f"{special_form} method must be named by symbol: (name [& args] & body)",
1440
            form=form.first,
1441
        )
1442
    method_name = form.first.name
1✔
1443

1444
    is_classmethod = _is_py_classmethod(form.first) or (
1✔
1445
        isinstance(form, IMeta) and _is_py_classmethod(form)
1446
    )
1447
    is_property = _is_py_property(form.first) or (
1✔
1448
        isinstance(form, IMeta) and _is_py_property(form)
1449
    )
1450
    is_staticmethod = _is_py_staticmethod(form.first) or (
1✔
1451
        isinstance(form, IMeta) and _is_py_staticmethod(form)
1452
    )
1453

1454
    if special_form == SpecialForm.REIFY and (is_classmethod or is_staticmethod):
1✔
1455
        raise ctx.AnalyzerException(
1✔
1456
            f"{special_form} does not support classmethod or staticmethod members",
1457
            form=form,
1458
        )
1459

1460
    if sum([is_classmethod, is_property, is_staticmethod]) not in {0, 1}:
1✔
1461
        raise ctx.AnalyzerException(
1✔
1462
            f"{special_form} member may be only one of: :classmethod, :property, "
1463
            "or :staticmethod",
1464
            form=form,
1465
        )
1466

1467
    args = runtime.nth(form, 1)
1✔
1468
    if not isinstance(args, vec.PersistentVector):
1✔
1469
        raise ctx.AnalyzerException(
1✔
1470
            f"{special_form} member arguments must be vector, not {type(args)}",
1471
            form=args,
1472
        )
1473

1474
    kwarg_meta = __fn_kwargs_support(ctx, form.first) or (
1✔
1475
        isinstance(form, IMeta) and __fn_kwargs_support(ctx, form)
1476
    )
1477
    kwarg_support = None if isinstance(kwarg_meta, bool) else kwarg_meta
1✔
1478

1479
    if is_classmethod:
1✔
1480
        return __deftype_classmethod(
1✔
1481
            form, ctx, method_name, args, kwarg_support=kwarg_support
1482
        )
1483
    elif is_property:
1✔
1484
        if kwarg_support is not None:
1✔
1485
            raise ctx.AnalyzerException(
1✔
1486
                f"{special_form} properties may not declare keyword argument support",
1487
                form=form,
1488
            )
1489

1490
        return __deftype_or_reify_property(form, ctx, method_name, args, special_form)
1✔
1491
    elif is_staticmethod:
1✔
1492
        return __deftype_staticmethod(
1✔
1493
            form, ctx, method_name, args, kwarg_support=kwarg_support
1494
        )
1495
    else:
1496
        return __deftype_or_reify_method(
1✔
1497
            form, ctx, method_name, args, special_form, kwarg_support=kwarg_support
1498
        )
1499

1500

1501
def __deftype_or_reify_method_node_from_arities(
1✔
1502
    form: Union[llist.PersistentList, ISeq],
1503
    ctx: AnalyzerContext,
1504
    arities: List[DefTypeMethodArity],
1505
    special_form: sym.Symbol,
1506
) -> DefTypeMember:
1507
    """Roll all of the collected `deftype*` or `reify*` arities up into a single
1508
    method node."""
1509
    assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
1✔
1510

1511
    fixed_arities: MutableSet[int] = set()
1✔
1512
    fixed_arity_for_variadic: Optional[int] = None
1✔
1513
    num_variadic = 0
1✔
1514
    for arity in arities:
1✔
1515
        if arity.is_variadic:
1✔
1516
            if num_variadic > 0:
1✔
1517
                raise ctx.AnalyzerException(
1✔
1518
                    f"{special_form} method may have at most 1 variadic arity",
1519
                    form=arity.form,
1520
                )
1521
            fixed_arity_for_variadic = arity.fixed_arity
1✔
1522
            num_variadic += 1
1✔
1523
        else:
1524
            if arity.fixed_arity in fixed_arities:
1✔
1525
                raise ctx.AnalyzerException(
1✔
1526
                    f"{special_form} may not have multiple methods with the same "
1527
                    "fixed arity",
1528
                    form=arity.form,
1529
                )
1530
            fixed_arities.add(arity.fixed_arity)
1✔
1531

1532
    if fixed_arity_for_variadic is not None and any(
1✔
1533
        fixed_arity_for_variadic < arity for arity in fixed_arities
1534
    ):
1535
        raise ctx.AnalyzerException(
1✔
1536
            "variadic arity may not have fewer fixed arity arguments than any other arities",
1537
            form=form,
1538
        )
1539

1540
    assert (
1✔
1541
        len(set(arity.name for arity in arities)) <= 1
1542
    ), "arities must have the same name defined"
1543

1544
    if len(arities) > 1 and any(arity.kwarg_support is not None for arity in arities):
1✔
1545
        raise ctx.AnalyzerException(
1✔
1546
            f"multi-arity {special_form} methods may not declare support for "
1547
            "keyword arguments",
1548
            form=form,
1549
        )
1550

1551
    return DefTypeMethod(
1✔
1552
        form=form,
1553
        name=arities[0].name,
1554
        max_fixed_arity=max(arity.fixed_arity for arity in arities),
1555
        arities=vec.vector(arities),
1556
        is_variadic=num_variadic == 1,
1557
        env=ctx.get_node_env(),
1558
    )
1559

1560

1561
def __deftype_or_reify_impls(  # pylint: disable=too-many-branches,too-many-locals  # noqa: MC0001
1✔
1562
    form: ISeq, ctx: AnalyzerContext, special_form: sym.Symbol
1563
) -> Tuple[List[DefTypeBase], List[DefTypeMember]]:
1564
    """Roll up `deftype*` and `reify*` declared bases and method implementations."""
1565
    assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
1✔
1566

1567
    if runtime.to_seq(form) is None:
1✔
1568
        return [], []
1✔
1569

1570
    if not isinstance(form.first, kw.Keyword) or form.first != IMPLEMENTS:
1✔
1571
        raise ctx.AnalyzerException(
1✔
1572
            f"{special_form} forms must declare which interfaces they implement",
1573
            form=form,
1574
        )
1575

1576
    implements = runtime.nth(form, 1)
1✔
1577
    if not isinstance(implements, vec.PersistentVector):
1✔
1578
        raise ctx.AnalyzerException(
1✔
1579
            f"{special_form} interfaces must be declared as "
1580
            ":implements [Interface1 Interface2 ...]",
1581
            form=implements,
1582
        )
1583

1584
    interface_names: MutableSet[sym.Symbol] = set()
1✔
1585
    interfaces = []
1✔
1586
    for iface in implements:
1✔
1587
        if not isinstance(iface, sym.Symbol):
1✔
1588
            raise ctx.AnalyzerException(
1✔
1589
                f"{special_form} interfaces must be symbols", form=iface
1590
            )
1591

1592
        if iface in interface_names:
1✔
1593
            raise ctx.AnalyzerException(
1✔
1594
                f"{special_form} interfaces may only appear once in :implements vector",
1595
                form=iface,
1596
            )
1597
        interface_names.add(iface)
1✔
1598

1599
        current_interface = _analyze_form(iface, ctx)
1✔
1600
        if not isinstance(current_interface, (MaybeClass, MaybeHostForm, VarRef)):
1✔
1601
            raise ctx.AnalyzerException(
1✔
1602
                f"{special_form} interface implementation must be an existing interface",
1603
                form=iface,
1604
            )
1605
        interfaces.append(current_interface)
1✔
1606

1607
    # Use the insertion-order preserving capabilities of a dictionary with 'True'
1608
    # keys to act as an ordered set of members we've seen. We don't want to register
1609
    # duplicates.
1610
    member_order = {}
1✔
1611
    methods: MutableMapping[str, List[DefTypeMethodArity]] = collections.defaultdict(
1✔
1612
        list
1613
    )
1614
    py_members: MutableMapping[str, DefTypePythonMember] = {}
1✔
1615
    for elem in runtime.nthrest(form, 2):
1✔
1616
        if not isinstance(elem, ISeq):
1✔
1617
            raise ctx.AnalyzerException(
1✔
1618
                f"{special_form} must consist of interface or protocol names and methods",
1619
                form=elem,
1620
            )
1621

1622
        member = __deftype_or_reify_prop_or_method_arity(elem, ctx, special_form)
1✔
1623
        member_order[member.name] = True
1✔
1624
        if isinstance(
1✔
1625
            member, (DefTypeClassMethod, DefTypeProperty, DefTypeStaticMethod)
1626
        ):
1627
            if member.name in py_members:
1✔
1628
                raise ctx.AnalyzerException(
1✔
1629
                    f"{special_form} class methods, properties, and static methods "
1630
                    "may only have one arity defined",
1631
                    form=elem,
1632
                    lisp_ast=member,
1633
                )
1634
            elif member.name in methods:
1✔
1635
                raise ctx.AnalyzerException(
1✔
1636
                    f"{special_form} class method, property, or static method name "
1637
                    "already defined as a method",
1638
                    form=elem,
1639
                    lisp_ast=member,
1640
                )
1641
            py_members[member.name] = member
1✔
1642
        else:
1643
            if member.name in py_members:
1✔
1644
                raise ctx.AnalyzerException(
1✔
1645
                    f"{special_form} method name already defined as a class method, "
1646
                    "property, or static method",
1647
                    form=elem,
1648
                    lisp_ast=member,
1649
                )
1650
            methods[member.name].append(member)
1✔
1651

1652
    members: List[DefTypeMember] = []
1✔
1653
    for member_name in member_order:
1✔
1654
        arities = methods.get(member_name)
1✔
1655
        if arities is not None:
1✔
1656
            members.append(
1✔
1657
                __deftype_or_reify_method_node_from_arities(
1658
                    form, ctx, arities, special_form
1659
                )
1660
            )
1661
            continue
1✔
1662

1663
        py_member = py_members.get(member_name)
1✔
1664
        assert py_member is not None, "Member must be a method or property"
1✔
1665
        members.append(py_member)
1✔
1666

1667
    return interfaces, members
1✔
1668

1669

1670
_var_is_protocol = _bool_meta_getter(VAR_IS_PROTOCOL_META_KEY)
1✔
1671

1672

1673
def __is_deftype_member(mem) -> bool:
1✔
1674
    """Return True if `mem` names a valid `deftype*` member."""
1675
    return inspect.isroutine(mem) or isinstance(mem, (property, staticmethod))
1✔
1676

1677

1678
def __is_reify_member(mem) -> bool:
1✔
1679
    """Return True if `mem` names a valid `reify*` member."""
1680
    return inspect.isroutine(mem) or isinstance(mem, property)
1✔
1681

1682

1683
if platform.python_implementation() == "CPython":
1✔
1684

1685
    def __is_type_weakref(tp: Type) -> bool:
1✔
1686
        return getattr(tp, "__weakrefoffset__", 0) > 0
1✔
1687

1688
else:
1689

1690
    def __is_type_weakref(tp: Type) -> bool:  # pylint: disable=unused-argument
1✔
1691
        return True
1✔
1692

1693

1694
def __get_artificially_abstract_members(
1✔
1695
    ctx: AnalyzerContext, special_form: sym.Symbol, interface: DefTypeBase
1696
) -> Set[str]:
1697
    if (
1✔
1698
        declared_abstract_members := _artificially_abstract_members(
1699
            cast(IMeta, interface.form)
1700
        )
1701
    ) is None:
1702
        return set()
1✔
1703

1704
    if (
1✔
1705
        not isinstance(declared_abstract_members, lset.PersistentSet)
1706
        or len(declared_abstract_members) == 0
1707
    ):
1708
        raise ctx.AnalyzerException(
1✔
1709
            f"{special_form} artificially abstract members must be a set of keywords",
1710
            form=interface.form,
1711
        )
1712

1713
    members = set()
1✔
1714
    for mem in declared_abstract_members:
1✔
1715
        if isinstance(mem, INamed):
1✔
1716
            if mem.ns is not None:
1✔
1717
                logger.warning(
1✔
1718
                    "Unexpected namespace for artificially abstract member to "
1719
                    f"{special_form}: {mem}"
1720
                )
1721
            members.add(mem.name)
1✔
1722
        elif isinstance(mem, str):
1✔
1723
            members.add(mem)
1✔
1724
        else:
1725
            raise ctx.AnalyzerException(
1✔
1726
                f"{special_form} artificially abstract member names must be one of: "
1727
                f"str, keyword, or symbol; got {type(mem)}",
1728
                form=interface.form,
1729
            )
1730
    return members
1✔
1731

1732

1733
@attr.define
1✔
1734
class _TypeAbstractness:
1✔
1735
    """
1736
    :ivar is_statically_verified_as_abstract: a boolean value which, if True,
1737
        indicates that all bases have been statically verified abstract; if False,
1738
        indicates at least one base could not be statically verified
1739
    :ivar artificially_abstract_supertypes: the set of all super-types which have
1740
        been marked as artificially abstract
1741
    :ivar supertype_already_weakref: if True, a supertype is already marked as
1742
        weakref and therefore the resulting type cannot add "__weakref__" to the
1743
        slots list to enable weakref support
1744
    """
1745

1746
    is_statically_verified_as_abstract: bool
1✔
1747
    artificially_abstract_supertypes: lset.PersistentSet[DefTypeBase]
1✔
1748
    supertype_already_weakref: bool
1✔
1749

1750

1751
def __deftype_and_reify_impls_are_all_abstract(  # pylint: disable=too-many-locals,too-many-statements
1✔
1752
    ctx: AnalyzerContext,
1753
    special_form: sym.Symbol,
1754
    fields: Iterable[str],
1755
    interfaces: Iterable[DefTypeBase],
1756
    members: Iterable[DefTypeMember],
1757
) -> _TypeAbstractness:
1758
    """Return an object indicating the abstractness of the `deftype*` or `reify*`
1759
    super-types.
1760

1761
    In certain cases, such as in macro definitions and potentially inside of
1762
    functions, the compiler will be unable to resolve the named super-type as an
1763
    object during compilation and these checks will need to be deferred to runtime.
1764
    In these cases, the compiler will wrap the emitted class in a decorator that
1765
    performs the checks when the class is compiled by the Python compiler.
1766

1767
    The Python ecosystem is much less strict with its use of `abc.ABC` to define
1768
    interfaces than Java (which has a native `interface` construct), so even in cases
1769
    where a type may be _in practice_ an interface or ABC, the compiler would not
1770
    permit you to declare such types as supertypes because they do not themselves
1771
    inherit from `abc.ABC`. In these cases, users can mark the type as artificially
1772
    abstract with the `:abstract` metadata key.
1773

1774
    For normal compile-time errors, an `AnalyzerException` will be raised."""
1775
    assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
1✔
1776

1777
    supertype_possibly_weakref = []
1✔
1778
    unverifiably_abstract = set()
1✔
1779
    artificially_abstract: Set[DefTypeBase] = set()
1✔
1780
    artificially_abstract_base_members: Set[str] = set()
1✔
1781
    is_member = {
1✔
1782
        SpecialForm.DEFTYPE: __is_deftype_member,
1783
        SpecialForm.REIFY: __is_reify_member,
1784
    }[special_form]
1785

1786
    field_names = frozenset(fields)
1✔
1787
    member_names = frozenset(deftype_or_reify_python_member_names(members))
1✔
1788
    all_member_names = field_names.union(member_names)
1✔
1789
    all_interface_methods: Set[str] = set()
1✔
1790
    for interface in interfaces:
1✔
1791
        if isinstance(interface, (MaybeClass, MaybeHostForm)):
1✔
1792
            interface_type = interface.target
1✔
1793
        else:
1794
            assert isinstance(
1✔
1795
                interface, VarRef
1796
            ), "Interface must be MaybeClass, MaybeHostForm, or VarRef"
1797
            if not interface.var.is_bound:
1✔
1798
                logger.log(
1✔
1799
                    TRACE,
1800
                    f"{special_form} interface Var '{interface.form}' is not bound "
1801
                    "and cannot be checked for abstractness; deferring to runtime",
1802
                )
1803
                unverifiably_abstract.add(interface)
1✔
1804
                if isinstance(interface.form, IMeta) and _is_artificially_abstract(
1✔
1805
                    interface.form
1806
                ):
1807
                    artificially_abstract.add(interface)
1✔
1808
                continue
1✔
1809

1810
            # Protocols are defined as maps, with the interface being simply a member
1811
            # of the map, denoted by the keyword `:interface`.
1812
            if _var_is_protocol(interface.var):
1✔
1813
                proto_map = interface.var.value
1✔
1814
                assert isinstance(proto_map, lmap.PersistentMap)
1✔
1815
                interface_type = proto_map.val_at(INTERFACE)
1✔
1816
            else:
1817
                interface_type = interface.var.value
1✔
1818

1819
        if interface_type is object:
1✔
1820
            continue
1✔
1821

1822
        if isinstance(interface.form, IMeta) and _is_artificially_abstract(
1✔
1823
            interface.form
1824
        ):
1825
            # Given that artificially abstract bases aren't real `abc.ABC`s and do
1826
            # not annotate their `abstractmethod`s, we can't assert right now that
1827
            # any the type will satisfy the artificially abstract base. However,
1828
            # we can collect any defined methods into a set for artificial bases
1829
            # and assert that any extra methods are included in that set below.
1830
            artificially_abstract.add(interface)
1✔
1831
            artificially_abstract_base_members.update(
1✔
1832
                map(
1833
                    lambda v: v[0],
1834
                    inspect.getmembers(interface_type, predicate=is_member),
1835
                )
1836
            )
1837
            # The meta key :abstract-members will give users the escape hatch to force
1838
            # the compiler to recognize those members as abstract members.
1839
            #
1840
            # When dealing with artificially abstract members, it may be the case
1841
            # that members of superclasses of a specific type don't actually declare
1842
            # abstract member methods expected by subclasses. One particular (major)
1843
            # instance is that of `io.IOBase` which does not declare "read" or "write"
1844
            # as abstract members but whose documentation declares both methods should
1845
            # be considered part of the interface.
1846
            artificially_abstract_base_members.update(
1✔
1847
                __get_artificially_abstract_members(ctx, special_form, interface)
1848
            )
1849
            supertype_possibly_weakref.append(__is_type_weakref(interface_type))
1✔
1850
        elif is_abstract(interface_type):
1✔
1851
            interface_names: FrozenSet[str] = interface_type.__abstractmethods__
1✔
1852
            interface_property_names: FrozenSet[str] = frozenset(
1✔
1853
                method
1854
                for method in interface_names
1855
                if isinstance(getattr(interface_type, method), property)
1856
            )
1857
            interface_method_names = interface_names - interface_property_names
1✔
1858
            if not interface_method_names.issubset(member_names):
1✔
1859
                missing_methods = ", ".join(interface_method_names - member_names)
1✔
1860
                raise ctx.AnalyzerException(
1✔
1861
                    f"{special_form} definition missing interface members for "
1862
                    f"interface {interface.form}: {missing_methods}",
1863
                    form=interface.form,
1864
                    lisp_ast=interface,
1865
                )
1866
            elif not interface_property_names.issubset(all_member_names):
1✔
1867
                missing_fields = ", ".join(interface_property_names - field_names)
1✔
1868
                raise ctx.AnalyzerException(
1✔
1869
                    f"{special_form} definition missing interface properties for "
1870
                    f"interface {interface.form}: {missing_fields}",
1871
                    form=interface.form,
1872
                    lisp_ast=interface,
1873
                )
1874

1875
            all_interface_methods.update(interface_names)
1✔
1876
            supertype_possibly_weakref.append(__is_type_weakref(interface_type))
1✔
1877
        else:
1878
            raise ctx.AnalyzerException(
1✔
1879
                f"{special_form} interface must be Python abstract class or object",
1880
                form=interface.form,
1881
                lisp_ast=interface,
1882
            )
1883

1884
    # We cannot compute if there are extra methods defined if there are any
1885
    # unverifiably abstract bases, so we just skip this check.
1886
    if unverifiably_abstract:
1✔
1887
        logger.warning(
1✔
1888
            f"Unable to verify abstractness for {special_form} supertype(s): "
1889
            f"{', '.join(str(e.var) for e in unverifiably_abstract)}"
1890
        )
1891
    else:
1892
        extra_methods = member_names - all_interface_methods - OBJECT_DUNDER_METHODS
1✔
1893
        if extra_methods and not extra_methods.issubset(
1✔
1894
            artificially_abstract_base_members
1895
        ):
1896
            expected_methods = (
1✔
1897
                all_interface_methods | artificially_abstract_base_members
1898
            ) - OBJECT_DUNDER_METHODS
1899
            expected_method_str = ", ".join(expected_methods)
1✔
1900
            extra_method_str = ", ".join(extra_methods)
1✔
1901
            raise ctx.AnalyzerException(
1✔
1902
                f"{special_form} definition for interface includes members not "
1903
                f"part of defined interfaces: {extra_method_str}; expected one of: "
1904
                f"{expected_method_str}"
1905
            )
1906

1907
    return _TypeAbstractness(
1✔
1908
        is_statically_verified_as_abstract=not unverifiably_abstract,
1909
        artificially_abstract_supertypes=lset.set(artificially_abstract),
1910
        supertype_already_weakref=any(supertype_possibly_weakref),
1911
    )
1912

1913

1914
__DEFTYPE_DEFAULT_SENTINEL = object()
1✔
1915

1916

1917
def _deftype_ast(  # pylint: disable=too-many-locals
1✔
1918
    form: ISeq, ctx: AnalyzerContext
1919
) -> DefType:
1920
    assert form.first == SpecialForm.DEFTYPE
1✔
1921

1922
    nelems = count(form)
1✔
1923
    if nelems < 3:
1✔
1924
        raise ctx.AnalyzerException(
1✔
1925
            "deftype forms must have 3 or more elements, as in: "
1926
            "(deftype* name fields :implements [bases+impls])",
1927
            form=form,
1928
        )
1929

1930
    name = runtime.nth(form, 1)
1✔
1931
    if not isinstance(name, sym.Symbol):
1✔
1932
        raise ctx.AnalyzerException(
1✔
1933
            f"deftype* names must be symbols, not {type(name)}", form=name
1934
        )
1935
    ctx.put_new_symbol(
1✔
1936
        name,
1937
        Binding(
1938
            form=name, name=name.name, local=LocalType.DEFTYPE, env=ctx.get_node_env()
1939
        ),
1940
        warn_if_unused=False,
1941
    )
1942

1943
    fields = runtime.nth(form, 2)
1✔
1944
    if not isinstance(fields, vec.PersistentVector):
1✔
1945
        raise ctx.AnalyzerException(
1✔
1946
            f"deftype* fields must be vector, not {type(fields)}", form=fields
1947
        )
1948

1949
    has_defaults = False
1✔
1950
    with ctx.new_symbol_table(name.name):
1✔
1951
        is_frozen = True
1✔
1952
        param_nodes = []
1✔
1953
        for field in fields:
1✔
1954
            if not isinstance(field, sym.Symbol):
1✔
1955
                raise ctx.AnalyzerException(
1✔
1956
                    "deftype* fields must be symbols", form=field
1957
                )
1958

1959
            field_default = (
1✔
1960
                Maybe(field.meta)
1961
                .map(
1962
                    lambda m: m.val_at(SYM_DEFAULT_META_KEY, __DEFTYPE_DEFAULT_SENTINEL)
1963
                )
1964
                .value
1965
            )
1966
            if not has_defaults and field_default is not __DEFTYPE_DEFAULT_SENTINEL:
1✔
1967
                has_defaults = True
1✔
1968
            elif has_defaults and field_default is __DEFTYPE_DEFAULT_SENTINEL:
1✔
1969
                raise ctx.AnalyzerException(
1✔
1970
                    "deftype* fields without defaults may not appear after fields "
1971
                    "without defaults",
1972
                    form=field,
1973
                )
1974

1975
            is_mutable = _is_mutable(field)
1✔
1976
            if is_mutable:
1✔
1977
                is_frozen = False
1✔
1978

1979
            binding = Binding(
1✔
1980
                form=field,
1981
                name=field.name,
1982
                local=LocalType.FIELD,
1983
                is_assignable=is_mutable,
1984
                env=ctx.get_node_env(),
1985
                tag=_tag_ast(_tag_meta(field), ctx),
1986
                init=(
1987
                    analyze_form(ctx, field_default)
1988
                    if field_default is not __DEFTYPE_DEFAULT_SENTINEL
1989
                    else None
1990
                ),
1991
            )
1992
            param_nodes.append(binding)
1✔
1993
            ctx.put_new_symbol(field, binding, warn_if_unused=False)
1✔
1994

1995
        interfaces, members = __deftype_or_reify_impls(
1✔
1996
            runtime.nthrest(form, 3), ctx, SpecialForm.DEFTYPE
1997
        )
1998
        type_abstractness = __deftype_and_reify_impls_are_all_abstract(
1✔
1999
            ctx, SpecialForm.DEFTYPE, map(lambda f: f.name, fields), interfaces, members
2000
        )
2001
        return DefType(
1✔
2002
            form=form,
2003
            name=name.name,
2004
            interfaces=vec.vector(interfaces),
2005
            fields=vec.vector(param_nodes),
2006
            members=vec.vector(members),
2007
            verified_abstract=type_abstractness.is_statically_verified_as_abstract,
2008
            artificially_abstract=type_abstractness.artificially_abstract_supertypes,
2009
            is_frozen=is_frozen,
2010
            use_weakref_slot=not type_abstractness.supertype_already_weakref,
2011
            env=ctx.get_node_env(pos=ctx.syntax_position),
2012
        )
2013

2014

2015
def _do_ast(form: ISeq, ctx: AnalyzerContext) -> Do:
1✔
2016
    assert form.first == SpecialForm.DO
1✔
2017
    statements, ret = _body_ast(form.rest, ctx)
1✔
2018
    return Do(
1✔
2019
        form=form,
2020
        statements=vec.vector(statements),
2021
        ret=ret,
2022
        use_var_indirection=_is_use_var_indirection(form.first),
2023
        env=ctx.get_node_env(pos=ctx.syntax_position),
2024
    )
2025

2026

2027
def __fn_method_ast(  # pylint: disable=too-many-locals
1✔
2028
    form: ISeq,
2029
    ctx: AnalyzerContext,
2030
    fnname: Optional[sym.Symbol] = None,
2031
    is_async: bool = False,
2032
) -> FnArity:
2033
    with ctx.new_symbol_table("fn-method", is_context_boundary=True):
1✔
2034
        params = form.first
1✔
2035
        if not isinstance(params, vec.PersistentVector):
1✔
2036
            raise ctx.AnalyzerException(
1✔
2037
                "function arity arguments must be a vector", form=params
2038
            )
2039
        return_tag = _tag_ast(_tag_meta(params), ctx)
1✔
2040

2041
        has_vargs, vargs_idx = False, 0
1✔
2042
        param_nodes = []
1✔
2043
        for i, s in enumerate(params):
1✔
2044
            if not isinstance(s, sym.Symbol):
1✔
2045
                raise ctx.AnalyzerException(
1✔
2046
                    "function arity parameter name must be a symbol", form=s
2047
                )
2048

2049
            if s == AMPERSAND:
1✔
2050
                has_vargs = True
1✔
2051
                vargs_idx = i
1✔
2052
                break
1✔
2053

2054
            binding = Binding(
1✔
2055
                form=s,
2056
                name=s.name,
2057
                local=LocalType.ARG,
2058
                tag=_tag_ast(_tag_meta(s), ctx),
2059
                arg_id=i,
2060
                is_variadic=False,
2061
                env=ctx.get_node_env(),
2062
            )
2063
            param_nodes.append(binding)
1✔
2064
            ctx.put_new_symbol(s, binding)
1✔
2065

2066
        if has_vargs:
1✔
2067
            try:
1✔
2068
                vargs_sym = params[vargs_idx + 1]
1✔
2069
                if not isinstance(vargs_sym, sym.Symbol):
1✔
2070
                    raise ctx.AnalyzerException(
1✔
2071
                        "function rest parameter name must be a symbol", form=vargs_sym
2072
                    )
2073

2074
                binding = Binding(
1✔
2075
                    form=vargs_sym,
2076
                    name=vargs_sym.name,
2077
                    local=LocalType.ARG,
2078
                    tag=_tag_ast(_tag_meta(vargs_sym), ctx),
2079
                    arg_id=vargs_idx + 1,
2080
                    is_variadic=True,
2081
                    env=ctx.get_node_env(),
2082
                )
2083
                param_nodes.append(binding)
1✔
2084
                ctx.put_new_symbol(vargs_sym, binding)
1✔
2085
            except IndexError:
1✔
2086
                raise ctx.AnalyzerException(
1✔
2087
                    "Expected variadic argument name after '&'", form=params
2088
                ) from None
2089

2090
        fn_loop_id = genname("fn_arity" if fnname is None else fnname.name)
1✔
2091
        with ctx.new_recur_point(fn_loop_id, param_nodes):
1✔
2092
            with ctx.new_func_ctx(
1✔
2093
                FunctionContext.ASYNC_FUNCTION if is_async else FunctionContext.FUNCTION
2094
            ), ctx.expr_pos():
2095
                stmts, ret = _body_ast(form.rest, ctx)
1✔
2096
            method = FnArity(
1✔
2097
                form=form,
2098
                loop_id=fn_loop_id,
2099
                params=vec.vector(param_nodes),
2100
                tag=return_tag,
2101
                is_variadic=has_vargs,
2102
                fixed_arity=len(param_nodes) - int(has_vargs),
2103
                body=Do(
2104
                    form=form.rest,
2105
                    statements=vec.vector(stmts),
2106
                    ret=ret,
2107
                    is_body=True,
2108
                    # Use the argument vector or first body statement, whichever
2109
                    # exists, for metadata.
2110
                    env=ctx.get_node_env(),
2111
                ),
2112
                # Use the argument vector for fetching line/col since the
2113
                # form itself is a sequence with no meaningful metadata.
2114
                env=ctx.get_node_env(),
2115
            )
2116
            method.visit(partial(_assert_recur_is_tail, ctx))
1✔
2117
            return method
1✔
2118

2119

2120
def __fn_kwargs_support(ctx: AnalyzerContext, o: IMeta) -> Optional[KeywordArgSupport]:
1✔
2121
    if o.meta is None:
1✔
2122
        return None
1✔
2123

2124
    kwarg_support = o.meta.val_at(SYM_KWARGS_META_KEY)
1✔
2125
    if kwarg_support is None:
1✔
2126
        return None
1✔
2127

2128
    try:
1✔
2129
        return KeywordArgSupport(kwarg_support)
1✔
2130
    except ValueError as e:
1✔
2131
        raise ctx.AnalyzerException(
1✔
2132
            "fn keyword argument support metadata :kwarg must be one of: #{:apply :collect}",
2133
            form=kwarg_support,
2134
        ) from e
2135

2136

2137
InlineMeta = Union[Callable, bool, None]
1✔
2138

2139

2140
@functools.singledispatch
1✔
2141
def __unquote_args(f: LispForm, _: FrozenSet[sym.Symbol]):
1✔
2142
    return f
1✔
2143

2144

2145
@__unquote_args.register(sym.Symbol)
1✔
2146
def __unquote_args_sym(f: sym.Symbol, args: FrozenSet[sym.Symbol]):
1✔
2147
    if f in args:
1✔
2148
        return llist.l(reader._UNQUOTE, f)
1✔
2149
    return f
1✔
2150

2151

2152
def _inline_fn_ast(
1✔
2153
    ctx: AnalyzerContext,
2154
    form: Union[llist.PersistentList, ISeq],
2155
    name: Optional[Binding],
2156
    arities: vec.PersistentVector[FnArity],
2157
    num_variadic: int,
2158
) -> Optional[Fn]:
2159
    if not ctx.should_generate_auto_inlines:
1✔
2160
        return None
1✔
2161

2162
    inline_arity = arities[0]
1✔
2163

2164
    if num_variadic != 0:
1✔
2165
        raise ctx.AnalyzerException(
1✔
2166
            "functions with variadic arity may not be inlined",
2167
            form=form,
2168
        )
2169

2170
    if len(arities) != 1:
1✔
2171
        raise ctx.AnalyzerException(
1✔
2172
            "multi-arity functions cannot be inlined",
2173
            form=form,
2174
        )
2175

2176
    if len(inline_arity.body.statements) > 0:
1✔
2177
        raise ctx.AnalyzerException(
1✔
2178
            "cannot generate an inline function for functions with more than one "
2179
            "body expression",
2180
            form=form,
2181
        )
2182

2183
    logger.log(
1✔
2184
        TRACE, f"Generating inline def for {name.name if name is not None else 'fn'}"
2185
    )
2186
    unquoted_form = reader._postwalk(
1✔
2187
        lambda f: __unquote_args(
2188
            f,
2189
            frozenset(binding.form for binding in inline_arity.params),
2190
        ),
2191
        inline_arity.body.ret.form,
2192
    )
2193
    macroed_form = reader.syntax_quote(unquoted_form)
1✔
2194
    inline_fn_form = llist.l(
1✔
2195
        SpecialForm.FN,
2196
        *([sym.symbol(genname(f"{name.name}-inline"))] if name is not None else []),
2197
        vec.vector(binding.form for binding in inline_arity.params),
2198
        macroed_form,
2199
    )
2200
    return _fn_ast(inline_fn_form, ctx)
1✔
2201

2202

2203
@_with_meta  # noqa: MC0001
1✔
2204
def _fn_ast(  # pylint: disable=too-many-locals,too-many-statements
1✔
2205
    form: Union[llist.PersistentList, ISeq], ctx: AnalyzerContext
2206
) -> Fn:
2207
    assert form.first == SpecialForm.FN
1✔
2208

2209
    idx = 1
1✔
2210

2211
    with ctx.new_symbol_table("fn", is_context_boundary=True):
1✔
2212
        try:
1✔
2213
            name = runtime.nth(form, idx)
1✔
2214
        except IndexError as e:
1✔
2215
            raise ctx.AnalyzerException(
1✔
2216
                "fn form must match: (fn* name? [arg*] body*) or (fn* name? method*)",
2217
                form=form,
2218
            ) from e
2219

2220
        name_node: Optional[Binding]
2221
        inline: InlineMeta
2222
        if isinstance(name, sym.Symbol):
1✔
2223
            name_node = Binding(
1✔
2224
                form=name, name=name.name, local=LocalType.FN, env=ctx.get_node_env()
2225
            )
2226
            is_async = _is_async(name) or isinstance(form, IMeta) and _is_async(form)
1✔
2227
            inline = (
1✔
2228
                _inline_meta(name) or isinstance(form, IMeta) and _inline_meta(form)
2229
            )
2230
            kwarg_support = (
1✔
2231
                __fn_kwargs_support(ctx, name)
2232
                or isinstance(form, IMeta)
2233
                and __fn_kwargs_support(ctx, form)
2234
            )
2235
            ctx.put_new_symbol(name, name_node, warn_if_unused=False)
1✔
2236
            idx += 1
1✔
2237
        elif isinstance(name, (llist.PersistentList, vec.PersistentVector)):
1✔
2238
            name = None
1✔
2239
            name_node = None
1✔
2240
            is_async = isinstance(form, IMeta) and _is_async(form)
1✔
2241
            inline = isinstance(form, IMeta) and _inline_meta(form)
1✔
2242
            kwarg_support = isinstance(form, IMeta) and __fn_kwargs_support(ctx, form)
1✔
2243
        else:
2244
            raise ctx.AnalyzerException(
1✔
2245
                "fn form must match: (fn* name? [arg*] body*) or (fn* name? method*)",
2246
                form=form,
2247
            )
2248

2249
        try:
1✔
2250
            arity_or_args = runtime.nth(form, idx)
1✔
2251
        except IndexError as e:
1✔
2252
            raise ctx.AnalyzerException(
1✔
2253
                "fn form expects either multiple arities or a vector of arguments",
2254
                form=form,
2255
            ) from e
2256

2257
        if isinstance(arity_or_args, llist.PersistentList):
1✔
2258
            arities = vec.vector(
1✔
2259
                map(
2260
                    lambda form: __fn_method_ast(
2261
                        form, ctx, fnname=name, is_async=is_async
2262
                    ),
2263
                    runtime.nthrest(form, idx),
2264
                )
2265
            )
2266
        elif isinstance(arity_or_args, vec.PersistentVector):
1✔
2267
            arities = vec.v(
1✔
2268
                __fn_method_ast(
2269
                    runtime.nthrest(form, idx), ctx, fnname=name, is_async=is_async
2270
                )
2271
            )
2272
        else:
2273
            raise ctx.AnalyzerException(
1✔
2274
                "fn form must match: (fn* name? [arg*] body*) or (fn* name? method*)",
2275
                form=form,
2276
            )
2277

2278
        nmethods = count(arities)
1✔
2279
        assert nmethods > 0, "fn must have at least one arity"
1✔
2280

2281
        if kwarg_support is not None and nmethods > 1:
1✔
2282
            raise ctx.AnalyzerException(
1✔
2283
                "multi-arity functions may not declare support for keyword arguments",
2284
                form=form,
2285
            )
2286

2287
        fixed_arities: MutableSet[int] = set()
1✔
2288
        fixed_arity_for_variadic: Optional[int] = None
1✔
2289
        num_variadic = 0
1✔
2290
        for arity in arities:
1✔
2291
            if arity.is_variadic:
1✔
2292
                if num_variadic > 0:
1✔
2293
                    raise ctx.AnalyzerException(
1✔
2294
                        "fn may have at most 1 variadic arity", form=arity.form
2295
                    )
2296
                fixed_arity_for_variadic = arity.fixed_arity
1✔
2297
                num_variadic += 1
1✔
2298
            else:
2299
                if arity.fixed_arity in fixed_arities:
1✔
2300
                    raise ctx.AnalyzerException(
1✔
2301
                        "fn may not have multiple methods with the same fixed arity",
2302
                        form=arity.form,
2303
                    )
2304
                fixed_arities.add(arity.fixed_arity)
1✔
2305

2306
        if fixed_arity_for_variadic is not None and any(
1✔
2307
            fixed_arity_for_variadic < arity for arity in fixed_arities
2308
        ):
2309
            raise ctx.AnalyzerException(
1✔
2310
                "variadic arity may not have fewer fixed arity arguments than any other arities",
2311
                form=form,
2312
            )
2313

2314
        return Fn(
1✔
2315
            form=form,
2316
            is_variadic=num_variadic == 1,
2317
            max_fixed_arity=max(node.fixed_arity for node in arities),
2318
            arities=arities,
2319
            local=name_node,
2320
            env=ctx.get_node_env(pos=ctx.syntax_position),
2321
            is_async=is_async,
2322
            kwarg_support=None if isinstance(kwarg_support, bool) else kwarg_support,
2323
            # Generate an inlineable (macro) variant of the current function if:
2324
            #  - the metadata for the fn contains a boolean `:inline` meta key
2325
            #  - the function has a single arity, composed of a single expression
2326
            #
2327
            # This node attribute (inline_fn) is not used for holding user provided
2328
            # inline functions. If a user provides an inline function, then that
2329
            # function will be compiled as part of the metadata already and will be
2330
            # found by the compiler on the associated Var without needing this code
2331
            # path at all.
2332
            inline_fn=(
2333
                _inline_fn_ast(ctx, form, name_node, arities, num_variadic)
2334
                if isinstance(inline, bool)
2335
                else None
2336
            ),
2337
        )
2338

2339

2340
def _host_call_ast(form: ISeq, ctx: AnalyzerContext) -> HostCall:
1✔
2341
    assert isinstance(form.first, sym.Symbol)
1✔
2342

2343
    method = form.first
1✔
2344
    assert isinstance(method, sym.Symbol), "host interop field must be a symbol"
1✔
2345
    assert method.name.startswith(".")
1✔
2346

2347
    if not count(form) >= 2:
1✔
2348
        raise ctx.AnalyzerException(
1✔
2349
            "host interop calls must be 2 or more elements long", form=form
2350
        )
2351

2352
    args, kwargs = _call_args_ast(runtime.nthrest(form, 2), ctx)
1✔
2353
    return HostCall(
1✔
2354
        form=form,
2355
        method=method.name[1:],
2356
        target=_analyze_form(runtime.nth(form, 1), ctx),
2357
        args=args,
2358
        kwargs=kwargs,
2359
        env=ctx.get_node_env(pos=ctx.syntax_position),
2360
    )
2361

2362

2363
def _host_prop_ast(form: ISeq, ctx: AnalyzerContext) -> HostField:
1✔
2364
    assert isinstance(form.first, sym.Symbol)
1✔
2365

2366
    field = form.first
1✔
2367
    assert isinstance(field, sym.Symbol), "host interop field must be a symbol"
1✔
2368

2369
    nelems = count(form)
1✔
2370
    assert field.name.startswith(".-")
1✔
2371

2372
    if field.name == ".-":
1✔
2373
        try:
1✔
2374
            field = runtime.nth(form, 2)
1✔
2375
        except IndexError as e:
1✔
2376
            raise ctx.AnalyzerException(
1✔
2377
                "host interop prop must be exactly 3 elems long: (.- target field)",
2378
                form=form,
2379
            ) from e
2380
        else:
2381
            if not isinstance(field, sym.Symbol):
1✔
2382
                raise ctx.AnalyzerException(
1✔
2383
                    "host interop field must be a symbol", form=form
2384
                )
2385

2386
        if not nelems == 3:
1✔
2387
            raise ctx.AnalyzerException(
1✔
2388
                "host interop prop must be exactly 3 elems long: (.- target field)",
2389
                form=form,
2390
            )
2391

2392
        return HostField(
1✔
2393
            form=form,
2394
            field=field.name,
2395
            target=_analyze_form(runtime.nth(form, 1), ctx),
2396
            is_assignable=True,
2397
            env=ctx.get_node_env(pos=ctx.syntax_position),
2398
        )
2399
    else:
2400
        if not nelems == 2:
1✔
2401
            raise ctx.AnalyzerException(
1✔
2402
                "host interop prop must be exactly 2 elements long: (.-field target)",
2403
                form=form,
2404
            )
2405

2406
        return HostField(
1✔
2407
            form=form,
2408
            field=field.name[2:],
2409
            target=_analyze_form(runtime.nth(form, 1), ctx),
2410
            is_assignable=True,
2411
            env=ctx.get_node_env(pos=ctx.syntax_position),
2412
        )
2413

2414

2415
def _host_interop_ast(form: ISeq, ctx: AnalyzerContext) -> Union[HostCall, HostField]:
1✔
2416
    assert form.first == SpecialForm.INTEROP_CALL
1✔
2417
    nelems = count(form)
1✔
2418
    assert nelems >= 3
1✔
2419

2420
    maybe_m_or_f = runtime.nth(form, 2)
1✔
2421
    if isinstance(maybe_m_or_f, sym.Symbol):
1✔
2422
        # The clojure.tools.analyzer spec is unclear about whether or not a form
2423
        # like (. target -field) should be emitted as a :host-field or as a
2424
        # :host-interop node. I have elected to emit :host-field, since it is
2425
        # more specific.
2426
        if maybe_m_or_f.name.startswith("-"):
1✔
2427
            if nelems != 3:
1✔
2428
                raise ctx.AnalyzerException(
1✔
2429
                    "host field accesses must be exactly 3 elements long", form=form
2430
                )
2431

2432
            return HostField(
1✔
2433
                form=form,
2434
                field=maybe_m_or_f.name[1:],
2435
                target=_analyze_form(runtime.nth(form, 1), ctx),
2436
                is_assignable=True,
2437
                env=ctx.get_node_env(pos=ctx.syntax_position),
2438
            )
2439
        else:
2440
            args, kwargs = _call_args_ast(runtime.nthrest(form, 3), ctx)
1✔
2441
            return HostCall(
1✔
2442
                form=form,
2443
                method=maybe_m_or_f.name,
2444
                target=_analyze_form(runtime.nth(form, 1), ctx),
2445
                args=args,
2446
                kwargs=kwargs,
2447
                env=ctx.get_node_env(pos=ctx.syntax_position),
2448
            )
2449
    elif isinstance(maybe_m_or_f, (llist.PersistentList, ISeq)):
1✔
2450
        # Likewise, I emit :host-call for forms like (. target (method arg1 ...)).
2451
        method = maybe_m_or_f.first
1✔
2452
        if not isinstance(method, sym.Symbol):
1✔
2453
            raise ctx.AnalyzerException(
1✔
2454
                "host call method must be a symbol", form=method
2455
            )
2456

2457
        args, kwargs = _call_args_ast(maybe_m_or_f.rest, ctx)
1✔
2458
        return HostCall(
1✔
2459
            form=form,
2460
            method=method.name[1:] if method.name.startswith("-") else method.name,
2461
            target=_analyze_form(runtime.nth(form, 1), ctx),
2462
            args=args,
2463
            kwargs=kwargs,
2464
            env=ctx.get_node_env(pos=ctx.syntax_position),
2465
        )
2466
    else:
2467
        raise ctx.AnalyzerException(
1✔
2468
            "host interop forms must take the form: "
2469
            "(. instance (method args*)), "
2470
            "(. instance method args*), "
2471
            "(. instance -field), ",
2472
            form=form,
2473
        )
2474

2475

2476
def _if_ast(form: ISeq, ctx: AnalyzerContext) -> If:
1✔
2477
    assert form.first == SpecialForm.IF
1✔
2478

2479
    nelems = count(form)
1✔
2480
    if nelems not in (3, 4):
1✔
2481
        raise ctx.AnalyzerException(
1✔
2482
            "if forms must have either 3 or 4 elements, as in: (if test then else?)",
2483
            form=form,
2484
        )
2485

2486
    with ctx.expr_pos():
1✔
2487
        test_node = _analyze_form(runtime.nth(form, 1), ctx)
1✔
2488

2489
    with ctx.parent_pos():
1✔
2490
        then_node = _analyze_form(runtime.nth(form, 2), ctx)
1✔
2491

2492
        if nelems == 4:
1✔
2493
            else_node = _analyze_form(runtime.nth(form, 3), ctx)
1✔
2494
        else:
2495
            else_node = _const_node(None, ctx)
1✔
2496

2497
    return If(
1✔
2498
        form=form,
2499
        test=test_node,
2500
        then=then_node,
2501
        else_=else_node,
2502
        env=ctx.get_node_env(pos=ctx.syntax_position),
2503
    )
2504

2505

2506
def _import_ast(form: ISeq, ctx: AnalyzerContext) -> Import:
1✔
2507
    assert form.first == SpecialForm.IMPORT
1✔
2508

2509
    aliases = []
1✔
2510
    for f in form.rest:
1✔
2511
        if isinstance(f, sym.Symbol):
1✔
2512
            module_name = f
1✔
2513
            module_alias = None
1✔
2514

2515
            ctx.put_new_symbol(
1✔
2516
                module_name,
2517
                Binding(
2518
                    form=module_name,
2519
                    name=module_name.name,
2520
                    local=LocalType.IMPORT,
2521
                    env=ctx.get_node_env(),
2522
                ),
2523
                symbol_table=ctx.symbol_table.context_boundary,
2524
            )
2525
        elif isinstance(f, vec.PersistentVector):
1✔
2526
            if len(f) != 3:
1✔
2527
                raise ctx.AnalyzerException(
1✔
2528
                    "import alias must take the form: [module :as alias]", form=f
2529
                )
2530
            module_name = f.val_at(0)  # type: ignore[assignment]
1✔
2531
            if not isinstance(module_name, sym.Symbol):
1✔
2532
                raise ctx.AnalyzerException(
1✔
2533
                    "Python module name must be a symbol", form=f
2534
                )
2535
            if not AS == f.val_at(1):
1✔
2536
                raise ctx.AnalyzerException(
1✔
2537
                    "expected :as alias for Python import", form=f
2538
                )
2539
            module_alias_sym = f.val_at(2)
1✔
2540
            if not isinstance(module_alias_sym, sym.Symbol):
1✔
2541
                raise ctx.AnalyzerException(
1✔
2542
                    "Python module alias must be a symbol", form=f
2543
                )
2544
            module_alias = module_alias_sym.name
1✔
2545

2546
            ctx.put_new_symbol(
1✔
2547
                module_alias_sym,
2548
                Binding(
2549
                    form=module_alias_sym,
2550
                    name=module_alias,
2551
                    local=LocalType.IMPORT,
2552
                    env=ctx.get_node_env(),
2553
                ),
2554
                symbol_table=ctx.symbol_table.context_boundary,
2555
            )
2556
        else:
2557
            raise ctx.AnalyzerException("symbol or vector expected for import*", form=f)
1✔
2558

2559
        aliases.append(
1✔
2560
            ImportAlias(
2561
                form=f,
2562
                name=module_name.name,
2563
                alias=module_alias,
2564
                env=ctx.get_node_env(),
2565
            )
2566
        )
2567

2568
    return Import(
1✔
2569
        form=form,
2570
        aliases=aliases,
2571
        env=ctx.get_node_env(pos=ctx.syntax_position),
2572
    )
2573

2574

2575
def __handle_macroexpanded_ast(
1✔
2576
    original: Union[llist.PersistentList, ISeq],
2577
    expanded: Union[ReaderForm, ISeq],
2578
    ctx: AnalyzerContext,
2579
) -> Node:
2580
    """Prepare the Lisp AST from macroexpanded and inlined code."""
2581
    if isinstance(expanded, IWithMeta) and isinstance(original, IMeta):
1✔
2582
        old_meta = expanded.meta
1✔
2583
        expanded = expanded.with_meta(
1✔
2584
            old_meta.cons(original.meta) if old_meta else original.meta
2585
        )
2586
    with ctx.expr_pos():
1✔
2587
        expanded_ast = _analyze_form(expanded, ctx)
1✔
2588

2589
    # Verify that macroexpanded code also does not have any non-tail
2590
    # recur forms
2591
    if ctx.recur_point is not None:
1✔
2592
        _assert_recur_is_tail(ctx, expanded_ast)
1✔
2593

2594
    return expanded_ast.assoc(
1✔
2595
        raw_forms=cast(vec.PersistentVector, expanded_ast.raw_forms).cons(original)
2596
    )
2597

2598

2599
def _do_warn_on_arity_mismatch(
1✔
2600
    fn: VarRef, form: Union[llist.PersistentList, ISeq], ctx: AnalyzerContext
2601
) -> None:
2602
    if ctx.warn_on_arity_mismatch and getattr(fn.var.value, "_basilisp_fn", False):
1✔
2603
        arities: Optional[Tuple[Union[int, kw.Keyword]]] = getattr(
1✔
2604
            fn.var.value, "arities", None
2605
        )
2606
        if arities is not None:
1✔
2607
            has_variadic = REST_KW in arities
1✔
2608
            fixed_arities = set(filter(lambda v: v != REST_KW, arities))
1✔
2609
            max_fixed_arity = max(fixed_arities) if fixed_arities else None
1✔
2610
            # This count could be off by 1 for cases where kwargs are being passed,
2611
            # but only Basilisp functions intended to be called by Python code
2612
            # (e.g. with a :kwargs strategy) should ever be called with kwargs,
2613
            # so this seems unlikely enough.
2614
            num_args = runtime.count(form.rest)
1✔
2615
            if has_variadic and (max_fixed_arity is None or num_args > max_fixed_arity):
1✔
2616
                return
1✔
2617
            if num_args not in fixed_arities:
1✔
2618
                report_arities = cast(Set[Union[int, str]], set(fixed_arities))
1✔
2619
                if has_variadic:
1✔
2620
                    report_arities.discard(cast(int, max_fixed_arity))
1✔
2621
                    report_arities.add(f"{max_fixed_arity}+")
1✔
2622
                loc = (
1✔
2623
                    f" ({fn.env.file}:{fn.env.line})"
2624
                    if fn.env.line is not None
2625
                    else f" ({fn.env.file})"
2626
                )
2627
                logger.warning(
1✔
2628
                    f"calling function {fn.var}{loc} with {num_args} arguments; "
2629
                    f"expected any of: {', '.join(sorted(map(str, report_arities)))}",
2630
                )
2631

2632

2633
def _invoke_ast(form: Union[llist.PersistentList, ISeq], ctx: AnalyzerContext) -> Node:
1✔
2634
    with ctx.expr_pos():
1✔
2635
        fn = _analyze_form(form.first, ctx)
1✔
2636

2637
    if fn.op == NodeOp.VAR and isinstance(fn, VarRef):
1✔
2638
        if _is_macro(fn.var) and ctx.should_macroexpand:
1✔
2639
            try:
1✔
2640
                macro_env = ctx.symbol_table.as_env_map()
1✔
2641
                expanded = fn.var.value(macro_env, form, *form.rest)
1✔
2642
                return __handle_macroexpanded_ast(form, expanded, ctx)
1✔
2643
            except Exception as e:
1✔
2644
                if isinstance(e, CompilerException) and (  # pylint: disable=no-member
1✔
2645
                    e.phase in {CompilerPhase.MACROEXPANSION, CompilerPhase.INLINING}
2646
                ):
2647
                    # Do not chain macroexpansion exceptions since they don't
2648
                    # actually add anything of value over the cause exception
2649
                    raise
1✔
2650
                raise CompilerException(
1✔
2651
                    "error occurred during macroexpansion",
2652
                    filename=ctx.filename,
2653
                    form=form,
2654
                    phase=CompilerPhase.MACROEXPANSION,
2655
                ) from e
2656
        elif (
1✔
2657
            ctx.should_inline_functions
2658
            and not (isinstance(form, IMeta) and _is_no_inline(form))
2659
            and fn.var.meta
2660
            and callable(fn.var.meta.get(SYM_INLINE_META_KW))
2661
        ):
2662
            # TODO: also consider whether or not the function(s) inside will be valid
2663
            #       if they are inlined (e.g. if the namespace or module is imported)
2664
            inline_fn = cast(Callable, fn.var.meta.get(SYM_INLINE_META_KW))
1✔
2665
            try:
1✔
2666
                expanded = inline_fn(*form.rest)
1✔
2667
                return __handle_macroexpanded_ast(form, expanded, ctx)
1✔
2668
            except Exception as e:
1✔
2669
                if isinstance(e, CompilerException) and (  # pylint: disable=no-member
1✔
2670
                    e.phase == CompilerPhase.INLINING
2671
                ):
UNCOV
2672
                    raise
×
2673
                raise CompilerException(
1✔
2674
                    "error occurred during inlining",
2675
                    filename=ctx.filename,
2676
                    form=form,
2677
                    phase=CompilerPhase.INLINING,
2678
                ) from e
2679

2680
        _do_warn_on_arity_mismatch(fn, form, ctx)
1✔
2681

2682
    args, kwargs = _call_args_ast(form.rest, ctx)
1✔
2683
    return Invoke(
1✔
2684
        form=form,
2685
        fn=fn,
2686
        args=args,
2687
        kwargs=kwargs,
2688
        env=ctx.get_node_env(pos=ctx.syntax_position),
2689
    )
2690

2691

2692
def _let_ast(form: ISeq, ctx: AnalyzerContext) -> Let:
1✔
2693
    assert form.first == SpecialForm.LET
1✔
2694
    nelems = count(form)
1✔
2695

2696
    if nelems < 2:
1✔
2697
        raise ctx.AnalyzerException(
1✔
2698
            "let forms must have bindings vector and 0 or more body forms", form=form
2699
        )
2700

2701
    bindings = runtime.nth(form, 1)
1✔
2702
    if not isinstance(bindings, vec.PersistentVector):
1✔
2703
        raise ctx.AnalyzerException("let bindings must be a vector", form=bindings)
1✔
2704
    elif len(bindings) % 2 != 0:
1✔
2705
        raise ctx.AnalyzerException(
1✔
2706
            "let bindings must appear in name-value pairs", form=bindings
2707
        )
2708

2709
    with ctx.new_symbol_table("let"):
1✔
2710
        binding_nodes = []
1✔
2711
        for name, value in partition(bindings, 2):
1✔
2712
            if not isinstance(name, sym.Symbol):
1✔
2713
                raise ctx.AnalyzerException(
1✔
2714
                    "let binding name must be a symbol", form=name
2715
                )
2716

2717
            binding = Binding(
1✔
2718
                form=name,
2719
                name=name.name,
2720
                local=LocalType.LET,
2721
                tag=_tag_ast(_tag_meta(name), ctx),
2722
                init=_analyze_form(value, ctx),
2723
                children=vec.v(INIT),
2724
                env=ctx.get_node_env(),
2725
            )
2726
            binding_nodes.append(binding)
1✔
2727
            ctx.put_new_symbol(name, binding)
1✔
2728

2729
        let_body = runtime.nthrest(form, 2)
1✔
2730
        stmts, ret = _body_ast(let_body, ctx)
1✔
2731
        return Let(
1✔
2732
            form=form,
2733
            bindings=vec.vector(binding_nodes),
2734
            body=Do(
2735
                form=let_body,
2736
                statements=vec.vector(stmts),
2737
                ret=ret,
2738
                is_body=True,
2739
                env=ctx.get_node_env(),
2740
            ),
2741
            env=ctx.get_node_env(pos=ctx.syntax_position),
2742
        )
2743

2744

2745
def __letfn_fn_body(form: ISeq, ctx: AnalyzerContext) -> Fn:
1✔
2746
    """Produce an `Fn` node for a `letfn*` special form.
2747

2748
    `letfn*` forms use `let*`-like bindings. Each function binding name is
2749
    added to the symbol table as a forward declaration before analyzing the
2750
    function body. The function bodies are defined as
2751

2752
        (fn* name
2753
          [...]
2754
          ...)
2755

2756
    When the `name` is added to the symbol table for the function, a warning
2757
    will be produced because it will previously have been defined in the
2758
    `letfn*` binding scope. This function adds `:no-warn-on-shadow` metadata to
2759
    the function name symbol to disable the compiler warning."""
2760
    fn_sym = form.first
1✔
2761

2762
    fn_name = runtime.nth(form, 1)
1✔
2763
    if not isinstance(fn_name, sym.Symbol):
1✔
2764
        raise ctx.AnalyzerException(
1✔
2765
            "letfn function name must be a symbol", form=fn_name
2766
        )
2767

2768
    fn_rest = runtime.nthrest(form, 2)
1✔
2769

2770
    fn_body = _analyze_form(
1✔
2771
        fn_rest.cons(
2772
            fn_name.with_meta(
2773
                (fn_name.meta or lmap.PersistentMap.empty()).assoc(
2774
                    SYM_NO_WARN_ON_SHADOW_META_KEY, True
2775
                )
2776
            )
2777
        ).cons(fn_sym),
2778
        ctx,
2779
    )
2780

2781
    if not isinstance(fn_body, Fn):
1✔
2782
        raise ctx.AnalyzerException(
1✔
2783
            "letfn bindings must be functions", form=form, lisp_ast=fn_body
2784
        )
2785

2786
    return fn_body
1✔
2787

2788

2789
def _letfn_ast(  # pylint: disable=too-many-locals
1✔
2790
    form: ISeq, ctx: AnalyzerContext
2791
) -> LetFn:
2792
    assert form.first == SpecialForm.LETFN
1✔
2793
    nelems = count(form)
1✔
2794

2795
    if nelems < 2:
1✔
2796
        raise ctx.AnalyzerException(
1✔
2797
            "letfn forms must have bindings vector and 0 or more body forms", form=form
2798
        )
2799

2800
    bindings = runtime.nth(form, 1)
1✔
2801
    if not isinstance(bindings, vec.PersistentVector):
1✔
2802
        raise ctx.AnalyzerException("letfn bindings must be a vector", form=bindings)
1✔
2803
    elif len(bindings) % 2 != 0:
1✔
2804
        raise ctx.AnalyzerException(
1✔
2805
            "letfn bindings must appear in name-value pairs", form=bindings
2806
        )
2807

2808
    with ctx.new_symbol_table("letfn"):
1✔
2809
        # Generate empty Binding nodes to put into the symbol table
2810
        # as forward declarations. All functions in letfn* forms may
2811
        # refer to all other functions regardless of order of definition.
2812
        empty_binding_nodes = []
1✔
2813
        for name, value in partition(bindings, 2):
1✔
2814
            if not isinstance(name, sym.Symbol):
1✔
2815
                raise ctx.AnalyzerException(
1✔
2816
                    "letfn binding name must be a symbol", form=name
2817
                )
2818

2819
            if not isinstance(value, llist.PersistentList):
1✔
2820
                raise ctx.AnalyzerException(
1✔
2821
                    "letfn binding value must be a list", form=value
2822
                )
2823

2824
            binding = Binding(
1✔
2825
                form=name,
2826
                name=name.name,
2827
                local=LocalType.LETFN,
2828
                init=_const_node(None, ctx),
2829
                children=vec.v(INIT),
2830
                env=ctx.get_node_env(),
2831
            )
2832
            empty_binding_nodes.append((name, value, binding))
1✔
2833
            ctx.put_new_symbol(
1✔
2834
                name,
2835
                binding,
2836
            )
2837

2838
        # Once we've generated all of the filler Binding nodes, analyze the
2839
        # function bodies and replace the Binding nodes with full nodes.
2840
        binding_nodes = []
1✔
2841
        for fn_name, fn_def, binding in empty_binding_nodes:
1✔
2842
            fn_body = __letfn_fn_body(fn_def, ctx)
1✔
2843
            new_binding = binding.assoc(init=fn_body)
1✔
2844
            binding_nodes.append(new_binding)
1✔
2845
            ctx.put_new_symbol(
1✔
2846
                fn_name,
2847
                new_binding,
2848
                warn_on_shadowed_name=False,
2849
                warn_on_shadowed_var=False,
2850
            )
2851

2852
        letfn_body = runtime.nthrest(form, 2)
1✔
2853
        stmts, ret = _body_ast(letfn_body, ctx)
1✔
2854
        return LetFn(
1✔
2855
            form=form,
2856
            bindings=vec.vector(binding_nodes),
2857
            body=Do(
2858
                form=letfn_body,
2859
                statements=vec.vector(stmts),
2860
                ret=ret,
2861
                is_body=True,
2862
                env=ctx.get_node_env(),
2863
            ),
2864
            env=ctx.get_node_env(pos=ctx.syntax_position),
2865
        )
2866

2867

2868
def _loop_ast(form: ISeq, ctx: AnalyzerContext) -> Loop:
1✔
2869
    assert form.first == SpecialForm.LOOP
1✔
2870
    nelems = count(form)
1✔
2871

2872
    if nelems < 2:
1✔
2873
        raise ctx.AnalyzerException(
1✔
2874
            "loop forms must have bindings vector and 0 or more body forms", form=form
2875
        )
2876

2877
    bindings = runtime.nth(form, 1)
1✔
2878
    if not isinstance(bindings, vec.PersistentVector):
1✔
2879
        raise ctx.AnalyzerException("loop bindings must be a vector", form=bindings)
1✔
2880
    elif len(bindings) % 2 != 0:
1✔
2881
        raise ctx.AnalyzerException(
1✔
2882
            "loop bindings must appear in name-value pairs", form=bindings
2883
        )
2884

2885
    loop_id = genname("loop")
1✔
2886
    with ctx.new_symbol_table(loop_id):
1✔
2887
        binding_nodes = []
1✔
2888
        for name, value in partition(bindings, 2):
1✔
2889
            if not isinstance(name, sym.Symbol):
1✔
2890
                raise ctx.AnalyzerException(
1✔
2891
                    "loop binding name must be a symbol", form=name
2892
                )
2893

2894
            binding = Binding(
1✔
2895
                form=name,
2896
                name=name.name,
2897
                local=LocalType.LOOP,
2898
                init=_analyze_form(value, ctx),
2899
                env=ctx.get_node_env(),
2900
            )
2901
            binding_nodes.append(binding)
1✔
2902
            ctx.put_new_symbol(name, binding)
1✔
2903

2904
        with ctx.new_recur_point(loop_id, binding_nodes):
1✔
2905
            loop_body = runtime.nthrest(form, 2)
1✔
2906
            stmts, ret = _body_ast(loop_body, ctx)
1✔
2907
            loop_node = Loop(
1✔
2908
                form=form,
2909
                bindings=vec.vector(binding_nodes),
2910
                body=Do(
2911
                    form=loop_body,
2912
                    statements=vec.vector(stmts),
2913
                    ret=ret,
2914
                    is_body=True,
2915
                    env=ctx.get_node_env(),
2916
                ),
2917
                loop_id=loop_id,
2918
                env=ctx.get_node_env(pos=ctx.syntax_position),
2919
            )
2920
            loop_node.visit(partial(_assert_recur_is_tail, ctx))
1✔
2921
            return loop_node
1✔
2922

2923

2924
def _quote_ast(form: ISeq, ctx: AnalyzerContext) -> Quote:
1✔
2925
    assert form.first == SpecialForm.QUOTE
1✔
2926
    nelems = count(form)
1✔
2927

2928
    if nelems != 2:
1✔
2929
        raise ctx.AnalyzerException(
1✔
2930
            "quote forms must have exactly two elements: (quote form)", form=form
2931
        )
2932

2933
    with ctx.quoted():
1✔
2934
        with ctx.expr_pos():
1✔
2935
            expr = _analyze_form(runtime.nth(form, 1), ctx)
1✔
2936
        assert isinstance(expr, Const), "Quoted expressions must yield :const nodes"
1✔
2937
        return Quote(
1✔
2938
            form=form,
2939
            expr=expr,
2940
            is_literal=True,
2941
            env=ctx.get_node_env(pos=ctx.syntax_position),
2942
        )
2943

2944

2945
def _assert_no_recur(ctx: AnalyzerContext, node: Node) -> None:
1✔
2946
    """Assert that `recur` forms do not appear in any position of this or
2947
    child AST nodes."""
2948
    if node.op == NodeOp.RECUR:
1✔
2949
        raise ctx.AnalyzerException(
1✔
2950
            "recur must appear in tail position", form=node.form, lisp_ast=node
2951
        )
2952
    elif node.op in {NodeOp.FN, NodeOp.LOOP}:
1✔
2953
        pass
1✔
2954
    else:
2955
        node.visit(partial(_assert_no_recur, ctx))
1✔
2956

2957

2958
def _assert_recur_is_tail(ctx: AnalyzerContext, node: Node) -> None:
1✔
2959
    """Assert that `recur` forms only appear in the tail position of this
2960
    or child AST nodes.
2961

2962
    `recur` forms may only appear in `do` nodes (both literal and synthetic
2963
    `do` nodes) and in either the :then or :else expression of an `if` node."""
2964
    if node.op == NodeOp.DO:
1✔
2965
        assert isinstance(node, Do)
1✔
2966
        for child in node.statements:
1✔
2967
            _assert_no_recur(ctx, child)
1✔
2968
        _assert_recur_is_tail(ctx, node.ret)
1✔
2969
    elif node.op in {
1✔
2970
        NodeOp.FN,
2971
        NodeOp.FN_ARITY,
2972
        NodeOp.DEFTYPE_METHOD,
2973
        NodeOp.DEFTYPE_METHOD_ARITY,
2974
    }:
2975
        assert isinstance(node, (Fn, FnArity, DefTypeMethod, DefTypeMethodArity))
1✔
2976
        node.visit(partial(_assert_recur_is_tail, ctx))
1✔
2977
    elif node.op == NodeOp.IF:
1✔
2978
        assert isinstance(node, If)
1✔
2979
        _assert_no_recur(ctx, node.test)
1✔
2980
        _assert_recur_is_tail(ctx, node.then)
1✔
2981
        _assert_recur_is_tail(ctx, node.else_)
1✔
2982
    elif node.op in {NodeOp.LET, NodeOp.LETFN}:
1✔
2983
        assert isinstance(node, (Let, LetFn))
1✔
2984
        for binding in node.bindings:
1✔
2985
            assert binding.init is not None
1✔
2986
            _assert_no_recur(ctx, binding.init)
1✔
2987
        _assert_recur_is_tail(ctx, node.body)
1✔
2988
    elif node.op == NodeOp.LOOP:
1✔
2989
        assert isinstance(node, Loop)
1✔
2990
        for binding in node.bindings:
1✔
2991
            assert binding.init is not None
1✔
2992
            _assert_no_recur(ctx, binding.init)
1✔
2993
    elif node.op == NodeOp.RECUR:
1✔
2994
        pass
1✔
2995
    elif node.op == NodeOp.REIFY:
1✔
2996
        assert isinstance(node, Reify)
1✔
2997
        for child in node.members:
1✔
2998
            _assert_recur_is_tail(ctx, child)
1✔
2999
    elif node.op == NodeOp.TRY:
1✔
3000
        assert isinstance(node, Try)
1✔
3001
        _assert_recur_is_tail(ctx, node.body)
1✔
3002
        for catch in node.catches:
1✔
3003
            _assert_recur_is_tail(ctx, catch)
1✔
3004
        if node.finally_:
1✔
3005
            _assert_no_recur(ctx, node.finally_)
1✔
3006
    else:
3007
        node.visit(partial(_assert_no_recur, ctx))
1✔
3008

3009

3010
def _recur_ast(form: ISeq, ctx: AnalyzerContext) -> Recur:
1✔
3011
    assert form.first == SpecialForm.RECUR
1✔
3012

3013
    if ctx.recur_point is None:
1✔
3014
        if ctx.should_allow_unresolved_symbols:
1✔
3015
            loop_id = genname("macroexpand-recur")
1✔
3016
        else:
3017
            raise ctx.AnalyzerException("no recur point defined for recur", form=form)
1✔
3018
    else:
3019
        if len(ctx.recur_point.args) != count(form.rest):
1✔
3020
            raise ctx.AnalyzerException(
1✔
3021
                "recur arity does not match last recur point arity", form=form
3022
            )
3023

3024
        loop_id = ctx.recur_point.loop_id
1✔
3025

3026
    with ctx.expr_pos():
1✔
3027
        exprs = vec.vector(map(lambda form: _analyze_form(form, ctx), form.rest))
1✔
3028

3029
    return Recur(form=form, exprs=exprs, loop_id=loop_id, env=ctx.get_node_env())
1✔
3030

3031

3032
@_with_meta
1✔
3033
def _reify_ast(form: ISeq, ctx: AnalyzerContext) -> Reify:
1✔
3034
    assert form.first == SpecialForm.REIFY
1✔
3035

3036
    nelems = count(form)
1✔
3037
    if nelems < 3:
1✔
3038
        raise ctx.AnalyzerException(
1✔
3039
            "reify forms must have 3 or more elements, as in: "
3040
            "(reify* :implements [bases+impls])",
3041
            form=form,
3042
        )
3043

3044
    with ctx.new_symbol_table("reify"):
1✔
3045
        interfaces, members = __deftype_or_reify_impls(
1✔
3046
            runtime.nthrest(form, 1), ctx, SpecialForm.REIFY
3047
        )
3048
        type_abstractness = __deftype_and_reify_impls_are_all_abstract(
1✔
3049
            ctx, SpecialForm.REIFY, (), interfaces, members
3050
        )
3051
        return Reify(
1✔
3052
            form=form,
3053
            interfaces=vec.vector(interfaces),
3054
            members=vec.vector(members),
3055
            verified_abstract=type_abstractness.is_statically_verified_as_abstract,
3056
            artificially_abstract=type_abstractness.artificially_abstract_supertypes,
3057
            use_weakref_slot=not type_abstractness.supertype_already_weakref,
3058
            env=ctx.get_node_env(pos=ctx.syntax_position),
3059
        )
3060

3061

3062
def _require_ast(form: ISeq, ctx: AnalyzerContext) -> Require:
1✔
3063
    assert form.first == SpecialForm.REQUIRE
1✔
3064

3065
    aliases = []
1✔
3066
    for f in form.rest:
1✔
3067
        if isinstance(f, sym.Symbol):
1✔
3068
            module_name = f
1✔
3069
            module_alias = None
1✔
3070
        elif isinstance(f, vec.PersistentVector):
1✔
3071
            if len(f) != 3:
1✔
3072
                raise ctx.AnalyzerException(
1✔
3073
                    "require alias must take the form: [namespace :as alias]", form=f
3074
                )
3075
            module_name = f.val_at(0)  # type: ignore[assignment]
1✔
3076
            if not isinstance(module_name, sym.Symbol):
1✔
3077
                raise ctx.AnalyzerException(
1✔
3078
                    "Basilisp namespace name must be a symbol", form=f
3079
                )
3080
            if not AS == f.val_at(1):
1✔
3081
                raise ctx.AnalyzerException(
1✔
3082
                    "expected :as alias for Basilisp alias", form=f
3083
                )
3084
            module_alias_sym = f.val_at(2)
1✔
3085
            if not isinstance(module_alias_sym, sym.Symbol):
1✔
3086
                raise ctx.AnalyzerException(
1✔
3087
                    "Basilisp namespace alias must be a symbol", form=f
3088
                )
3089
            module_alias = module_alias_sym.name
1✔
3090
        else:
3091
            raise ctx.AnalyzerException(
1✔
3092
                "symbol or vector expected for require*", form=f
3093
            )
3094

3095
        aliases.append(
1✔
3096
            RequireAlias(
3097
                form=f,
3098
                name=module_name.name,
3099
                alias=module_alias,
3100
                env=ctx.get_node_env(),
3101
            )
3102
        )
3103

3104
    return Require(
1✔
3105
        form=form,
3106
        aliases=aliases,
3107
        env=ctx.get_node_env(pos=ctx.syntax_position),
3108
    )
3109

3110

3111
def _set_bang_ast(form: ISeq, ctx: AnalyzerContext) -> SetBang:
1✔
3112
    assert form.first == SpecialForm.SET_BANG
1✔
3113
    nelems = count(form)
1✔
3114

3115
    if nelems != 3:
1✔
3116
        raise ctx.AnalyzerException(
1✔
3117
            "set! forms must contain exactly 3 elements: (set! target value)", form=form
3118
        )
3119

3120
    with ctx.expr_pos():
1✔
3121
        target = _analyze_form(runtime.nth(form, 1), ctx)
1✔
3122

3123
    if not isinstance(target, Assignable):
1✔
3124
        raise ctx.AnalyzerException(
1✔
3125
            f"cannot set! targets of type {type(target)}", form=target
3126
        )
3127

3128
    if not target.is_assignable:
1✔
3129
        raise ctx.AnalyzerException(
1✔
3130
            "cannot set! target which is not assignable",
3131
            form=form,
3132
            lisp_ast=cast(Node, target),
3133
        )
3134

3135
    # Vars may only be set if they are (1) dynamic, and (2) already have a thread
3136
    # binding established via the `binding` macro in Basilisp or by manually pushing
3137
    # thread bindings (e.g. by runtime.push_thread_bindings).
3138
    #
3139
    # We can (generally) establish statically whether a Var is dynamic at compile time,
3140
    # but establishing whether or not a Var will be thread-bound by the time a set!
3141
    # is executed is much more challenging. Given the dynamic nature of the language,
3142
    # it is simply easier to emit warnings for these potentially invalid cases to let
3143
    # users fix the problem.
3144
    if isinstance(target, VarRef):
1✔
3145
        if ctx.warn_on_non_dynamic_set and not target.var.dynamic:
1✔
3146
            logger.warning(f"set! target {target.var} is not marked as dynamic")
1✔
3147
        elif not target.var.is_thread_bound:
1✔
3148
            # This case is way more likely to result in noise, so just emit at debug.
3149
            logger.debug(f"set! target {target.var} is not marked as thread-bound")
1✔
3150

3151
    with ctx.expr_pos():
1✔
3152
        val = _analyze_form(runtime.nth(form, 2), ctx)
1✔
3153

3154
    return SetBang(
1✔
3155
        form=form,
3156
        target=target,
3157
        val=val,
3158
        env=ctx.get_node_env(pos=ctx.syntax_position),
3159
    )
3160

3161

3162
def _throw_ast(form: ISeq, ctx: AnalyzerContext) -> Throw:
1✔
3163
    assert form.first == SpecialForm.THROW
1✔
3164
    nelems = count(form)
1✔
3165

3166
    if nelems < 2 or nelems > 3:
1✔
3167
        raise ctx.AnalyzerException(
1✔
3168
            "throw forms must contain exactly 2 or 3 elements: (throw exc [cause])",
3169
            form=form,
3170
        )
3171

3172
    with ctx.expr_pos():
1✔
3173
        exc = _analyze_form(runtime.nth(form, 1), ctx)
1✔
3174

3175
    if nelems == 3:
1✔
3176
        with ctx.expr_pos():
1✔
3177
            cause = _analyze_form(runtime.nth(form, 2), ctx)
1✔
3178
    else:
3179
        cause = None
1✔
3180

3181
    return Throw(
1✔
3182
        form=form,
3183
        exception=exc,
3184
        cause=cause,
3185
        env=ctx.get_node_env(pos=ctx.syntax_position),
3186
    )
3187

3188

3189
def _catch_ast(form: ISeq, ctx: AnalyzerContext) -> Catch:
1✔
3190
    assert form.first == SpecialForm.CATCH
1✔
3191
    nelems = count(form)
1✔
3192

3193
    if nelems < 4:
1✔
3194
        raise ctx.AnalyzerException(
1✔
3195
            "catch forms must contain at least 4 elements: (catch class local body*)",
3196
            form=form,
3197
        )
3198

3199
    catch_cls = _analyze_form(runtime.nth(form, 1), ctx)
1✔
3200
    if not isinstance(catch_cls, (MaybeClass, MaybeHostForm)):
1✔
3201
        raise ctx.AnalyzerException(
1✔
3202
            "catch forms must name a class type to catch", form=catch_cls
3203
        )
3204

3205
    local_name = runtime.nth(form, 2)
1✔
3206
    if not isinstance(local_name, sym.Symbol):
1✔
3207
        raise ctx.AnalyzerException("catch local must be a symbol", form=local_name)
1✔
3208

3209
    with ctx.new_symbol_table("catch"):
1✔
3210
        catch_binding = Binding(
1✔
3211
            form=local_name,
3212
            name=local_name.name,
3213
            local=LocalType.CATCH,
3214
            env=ctx.get_node_env(),
3215
        )
3216
        ctx.put_new_symbol(local_name, catch_binding)
1✔
3217

3218
        catch_body = runtime.nthrest(form, 3)
1✔
3219
        catch_statements, catch_ret = _body_ast(catch_body, ctx)
1✔
3220
        return Catch(
1✔
3221
            form=form,
3222
            class_=catch_cls,
3223
            local=catch_binding,
3224
            body=Do(
3225
                form=catch_body,
3226
                statements=vec.vector(catch_statements),
3227
                ret=catch_ret,
3228
                is_body=True,
3229
                env=ctx.get_node_env(),
3230
            ),
3231
            env=ctx.get_node_env(),
3232
        )
3233

3234

3235
def _try_ast(form: ISeq, ctx: AnalyzerContext) -> Try:
1✔
3236
    assert form.first == SpecialForm.TRY
1✔
3237

3238
    try_exprs = []
1✔
3239
    catches = []
1✔
3240
    finally_: Optional[Do] = None
1✔
3241
    for expr in form.rest:
1✔
3242
        if isinstance(expr, (llist.PersistentList, ISeq)):
1✔
3243
            if expr.first == SpecialForm.CATCH:
1✔
3244
                if finally_:
1✔
3245
                    raise ctx.AnalyzerException(
1✔
3246
                        "catch forms may not appear after finally forms in a try",
3247
                        form=expr,
3248
                    )
3249
                catches.append(_catch_ast(expr, ctx))
1✔
3250
                continue
1✔
3251
            elif expr.first == SpecialForm.FINALLY:
1✔
3252
                if finally_ is not None:
1✔
3253
                    raise ctx.AnalyzerException(
1✔
3254
                        "try forms may not contain multiple finally forms", form=expr
3255
                    )
3256
                # Finally values are never returned
3257
                with ctx.stmt_pos():
1✔
3258
                    *finally_stmts, finally_ret = map(
1✔
3259
                        lambda form: _analyze_form(form, ctx), expr.rest
3260
                    )
3261
                finally_ = Do(
1✔
3262
                    form=expr.rest,
3263
                    statements=vec.vector(finally_stmts),
3264
                    ret=finally_ret,
3265
                    is_body=True,
3266
                    env=ctx.get_node_env(
3267
                        pos=NodeSyntacticPosition.STMT,
3268
                    ),
3269
                )
3270
                continue
1✔
3271

3272
        lisp_node = _analyze_form(expr, ctx)
1✔
3273

3274
        if catches:
1✔
3275
            raise ctx.AnalyzerException(
1✔
3276
                "try body expressions may not appear after catch forms", form=expr
3277
            )
3278
        if finally_:
1✔
3279
            raise ctx.AnalyzerException(
1✔
3280
                "try body expressions may not appear after finally forms", form=expr
3281
            )
3282

3283
        try_exprs.append(lisp_node)
1✔
3284

3285
    assert all(
1✔
3286
        isinstance(node, Catch) for node in catches
3287
    ), "All catch statements must be catch ops"
3288

3289
    *try_statements, try_ret = try_exprs
1✔
3290
    return Try(
1✔
3291
        form=form,
3292
        body=Do(
3293
            form=form,
3294
            statements=vec.vector(try_statements),
3295
            ret=try_ret,
3296
            is_body=True,
3297
            env=ctx.get_node_env(pos=ctx.syntax_position),
3298
        ),
3299
        catches=vec.vector(catches),
3300
        finally_=finally_,
3301
        children=(
3302
            vec.v(BODY, CATCHES, FINALLY)
3303
            if finally_ is not None
3304
            else vec.v(BODY, CATCHES)
3305
        ),
3306
        env=ctx.get_node_env(pos=ctx.syntax_position),
3307
    )
3308

3309

3310
def _var_ast(form: ISeq, ctx: AnalyzerContext) -> VarRef:
1✔
3311
    assert form.first == SpecialForm.VAR
1✔
3312

3313
    nelems = count(form)
1✔
3314
    if nelems != 2:
1✔
3315
        raise ctx.AnalyzerException(
1✔
3316
            "var special forms must contain 2 elements: (var sym)", form=form
3317
        )
3318

3319
    var_sym = runtime.nth(form, 1)
1✔
3320
    if not isinstance(var_sym, sym.Symbol):
1✔
3321
        raise ctx.AnalyzerException("vars may only be resolved for symbols", form=form)
1✔
3322

3323
    if var_sym.ns is None:
1✔
3324
        var = runtime.resolve_var(sym.symbol(var_sym.name, ctx.current_ns.name))
1✔
3325
    else:
3326
        var = runtime.resolve_var(var_sym)
1✔
3327

3328
    if var is None:
1✔
3329
        raise ctx.AnalyzerException(f"cannot resolve var {var_sym}", form=form)
1✔
3330

3331
    return VarRef(
1✔
3332
        form=form,
3333
        var=var,
3334
        return_var=True,
3335
        env=ctx.get_node_env(pos=ctx.syntax_position),
3336
    )
3337

3338

3339
def _yield_ast(form: ISeq, ctx: AnalyzerContext) -> Yield:
1✔
3340
    assert form.first == SpecialForm.YIELD
1✔
3341

3342
    if ctx.func_ctx is None:
1✔
3343
        raise ctx.AnalyzerException(
1✔
3344
            "yield forms may not appear in function context", form=form
3345
        )
3346

3347
    nelems = count(form)
1✔
3348
    if nelems not in {1, 2}:
1✔
3349
        raise ctx.AnalyzerException(
1✔
3350
            "yield forms must contain 1 or 2 elements, as in: (yield [expr])", form=form
3351
        )
3352

3353
    if nelems == 2:
1✔
3354
        with ctx.expr_pos():
1✔
3355
            expr = _analyze_form(runtime.nth(form, 1), ctx)
1✔
3356
        return Yield(
1✔
3357
            form=form,
3358
            expr=expr,
3359
            env=ctx.get_node_env(pos=ctx.syntax_position),
3360
        )
3361
    else:
3362
        return Yield.expressionless(form, ctx.get_node_env(pos=ctx.syntax_position))
1✔
3363

3364

3365
SpecialFormHandler = Callable[[T_form, AnalyzerContext], SpecialFormNode]
1✔
3366
_SPECIAL_FORM_HANDLERS: Mapping[sym.Symbol, SpecialFormHandler] = {
1✔
3367
    SpecialForm.AWAIT: _await_ast,
3368
    SpecialForm.DEF: _def_ast,
3369
    SpecialForm.DO: _do_ast,
3370
    SpecialForm.DEFTYPE: _deftype_ast,
3371
    SpecialForm.FN: _fn_ast,
3372
    SpecialForm.IF: _if_ast,
3373
    SpecialForm.IMPORT: _import_ast,
3374
    SpecialForm.INTEROP_CALL: _host_interop_ast,
3375
    SpecialForm.LET: _let_ast,
3376
    SpecialForm.LETFN: _letfn_ast,
3377
    SpecialForm.LOOP: _loop_ast,
3378
    SpecialForm.QUOTE: _quote_ast,
3379
    SpecialForm.RECUR: _recur_ast,
3380
    SpecialForm.REIFY: _reify_ast,
3381
    SpecialForm.REQUIRE: _require_ast,
3382
    SpecialForm.SET_BANG: _set_bang_ast,
3383
    SpecialForm.THROW: _throw_ast,
3384
    SpecialForm.TRY: _try_ast,
3385
    SpecialForm.VAR: _var_ast,
3386
    SpecialForm.YIELD: _yield_ast,
3387
}
3388

3389

3390
##################
3391
# Data Structures
3392
##################
3393

3394

3395
@_analyze_form.register(llist.PersistentList)
1✔
3396
@_analyze_form.register(ISeq)
1✔
3397
@_with_loc
1✔
3398
def _list_node(form: ISeq, ctx: AnalyzerContext) -> Node:
1✔
3399
    if form == llist.PersistentList.empty():
1✔
3400
        with ctx.quoted():
1✔
3401
            return _const_node(form, ctx)
1✔
3402

3403
    if ctx.is_quoted:
1✔
3404
        return _const_node(form, ctx)
1✔
3405

3406
    s = form.first
1✔
3407
    if isinstance(s, sym.Symbol):
1✔
3408
        handle_special_form = _SPECIAL_FORM_HANDLERS.get(s)
1✔
3409
        if handle_special_form is not None:
1✔
3410
            return handle_special_form(form, ctx)
1✔
3411
        elif s.name.startswith(".-"):
1✔
3412
            return _host_prop_ast(form, ctx)
1✔
3413
        elif s.name.startswith(".") and s.name != _DOUBLE_DOT_MACRO_NAME:
1✔
3414
            return _host_call_ast(form, ctx)
1✔
3415

3416
    return _invoke_ast(form, ctx)
1✔
3417

3418

3419
def _resolve_nested_symbol(ctx: AnalyzerContext, form: sym.Symbol) -> HostField:
1✔
3420
    """Resolve an attribute by recursively accessing the parent object
3421
    as if it were its own namespaced symbol."""
3422
    assert form.ns is not None
1✔
3423
    assert "." in form.ns
1✔
3424

3425
    parent_ns, parent_name = form.ns.rsplit(".", maxsplit=1)
1✔
3426
    parent = sym.symbol(parent_name, ns=parent_ns)
1✔
3427
    parent_node = __resolve_namespaced_symbol(ctx, parent)
1✔
3428

3429
    return HostField(
1✔
3430
        form=form,
3431
        field=form.name,
3432
        target=parent_node,
3433
        is_assignable=True,
3434
        env=ctx.get_node_env(pos=ctx.syntax_position),
3435
    )
3436

3437

3438
def __resolve_namespaced_symbol_in_ns(
1✔
3439
    ctx: AnalyzerContext,
3440
    which_ns: runtime.Namespace,
3441
    form: sym.Symbol,
3442
) -> Optional[Union[MaybeHostForm, VarRef]]:
3443
    """Resolve the symbol `form` in the context of the Namespace `which_ns`. If
3444
    `allow_fuzzy_macroexpansion_matching` is True and no match is made on existing
3445
    imports, import aliases, or namespace aliases, then attempt to match the
3446
    namespace portion"""
3447
    assert form.ns is not None
1✔
3448

3449
    ns_sym = sym.symbol(form.ns)
1✔
3450
    if ns_sym in which_ns.imports or ns_sym in which_ns.import_aliases:
1✔
3451
        # Fetch the full namespace name for the aliased namespace/module.
3452
        # We don't need this for actually generating the link later, but
3453
        # we _do_ need it for fetching a reference to the module to check
3454
        # for membership.
3455
        if ns_sym in which_ns.import_aliases:
1✔
3456
            ns = which_ns.import_aliases[ns_sym]
1✔
3457
            assert ns is not None
1✔
3458
            ns_name = ns.name
1✔
3459
        else:
3460
            ns_name = ns_sym.name
1✔
3461

3462
        safe_module_name = munge(ns_name)
1✔
3463
        assert (
1✔
3464
            safe_module_name in sys.modules
3465
        ), f"Module '{safe_module_name}' is not imported"
3466
        ns_module = sys.modules[safe_module_name]
1✔
3467
        safe_name = munge(form.name)
1✔
3468

3469
        # Try without allowing builtins first
3470
        if safe_name in vars(ns_module):
1✔
3471
            return MaybeHostForm(
1✔
3472
                form=form,
3473
                class_=munge(ns_sym.name),
3474
                field=safe_name,
3475
                target=vars(ns_module)[safe_name],
3476
                env=ctx.get_node_env(pos=ctx.syntax_position),
3477
            )
3478

3479
        # Then allow builtins
3480
        safe_name = munge(form.name, allow_builtins=True)
1✔
3481
        if safe_name not in vars(ns_module):
1✔
3482
            raise ctx.AnalyzerException("can't identify aliased form", form=form)
1✔
3483

3484
        # Aliased imports generate code which uses the import alias, so we
3485
        # don't need to care if this is an import or an alias.
3486
        return MaybeHostForm(
1✔
3487
            form=form,
3488
            class_=munge(ns_sym.name),
3489
            field=safe_name,
3490
            target=vars(ns_module)[safe_name],
3491
            env=ctx.get_node_env(pos=ctx.syntax_position),
3492
        )
3493
    elif ns_sym in which_ns.aliases:
1✔
3494
        aliased_ns: runtime.Namespace = which_ns.aliases[ns_sym]
1✔
3495
        v = Var.find_in_ns(aliased_ns, sym.symbol(form.name))
1✔
3496
        if v is None:
1✔
3497
            raise ctx.AnalyzerException(
1✔
3498
                f"unable to resolve symbol '{sym.symbol(form.name, ns_sym.name)}' in this context",
3499
                form=form,
3500
            )
3501
        elif v.meta is not None and v.meta.val_at(SYM_PRIVATE_META_KEY, False):
1✔
3502
            raise ctx.AnalyzerException(
1✔
3503
                f"cannot resolve private Var {form.name} from namespace {form.ns}",
3504
                form=form,
3505
            )
3506
        return VarRef(
1✔
3507
            form=form,
3508
            var=v,
3509
            env=ctx.get_node_env(pos=ctx.syntax_position),
3510
        )
3511

3512
    return None
1✔
3513

3514

3515
def __resolve_namespaced_symbol(  # pylint: disable=too-many-branches  # noqa: MC0001
1✔
3516
    ctx: AnalyzerContext, form: sym.Symbol
3517
) -> Union[Const, HostField, MaybeClass, MaybeHostForm, VarRef]:
3518
    """Resolve a namespaced symbol into a Python name or Basilisp Var."""
3519
    assert form.ns is not None
1✔
3520

3521
    current_ns = ctx.current_ns
1✔
3522
    if form.ns == current_ns.name:
1✔
3523
        v = current_ns.find(sym.symbol(form.name))
1✔
3524
        if v is not None:
1✔
3525
            return VarRef(
1✔
3526
                form=form,
3527
                var=v,
3528
                env=ctx.get_node_env(pos=ctx.syntax_position),
3529
            )
3530
    elif form.ns == _BUILTINS_NS:
1✔
3531
        class_ = munge(form.name, allow_builtins=True)
1✔
3532
        target = getattr(builtins, class_, None)
1✔
3533
        if target is None:
1✔
3534
            raise ctx.AnalyzerException(
1✔
3535
                f"cannot resolve builtin function '{class_}'", form=form
3536
            )
3537
        return MaybeClass(
1✔
3538
            form=form,
3539
            class_=class_,
3540
            target=target,
3541
            env=ctx.get_node_env(pos=ctx.syntax_position),
3542
        )
3543

3544
    v = Var.find(form)
1✔
3545
    if v is not None:
1✔
3546
        # Disallow global references to Vars defined with :private metadata
3547
        if v.meta is not None and v.meta.val_at(SYM_PRIVATE_META_KEY, False):
1✔
3548
            raise ctx.AnalyzerException(
1✔
3549
                f"cannot resolve private Var {form.name} from namespace {form.ns}",
3550
                form=form,
3551
            )
3552
        return VarRef(form=form, var=v, env=ctx.get_node_env(pos=ctx.syntax_position))
1✔
3553

3554
    if "." in form.name and form.name != _DOUBLE_DOT_MACRO_NAME:
1✔
3555
        raise ctx.AnalyzerException(
1✔
3556
            "symbol names may not contain the '.' operator", form=form
3557
        )
3558

3559
    resolved = __resolve_namespaced_symbol_in_ns(ctx, current_ns, form)
1✔
3560
    if resolved is not None:
1✔
3561
        return resolved
1✔
3562

3563
    if "." in form.ns:
1✔
3564
        try:
1✔
3565
            return _resolve_nested_symbol(ctx, form)
1✔
3566
        except CompilerException:
1✔
3567
            raise ctx.AnalyzerException(
1✔
3568
                f"unable to resolve symbol '{form}' in this context", form=form
3569
            ) from None
3570
    elif ctx.should_allow_unresolved_symbols:
1✔
3571
        return _const_node(form, ctx)
1✔
3572

3573
    # Imports and requires nested in function definitions, method definitions, and
3574
    # `(do ...)` forms are not statically resolvable, since they haven't necessarily
3575
    # been imported and we want to minimize side-effecting from the compiler. In these
3576
    # cases, we merely verify that we've seen the symbol before and defer to runtime
3577
    # checks by the Python VM to verify that the import or require is legitimate.
3578
    maybe_import_or_require_sym = sym.symbol(form.ns)
1✔
3579
    maybe_import_or_require_entry = ctx.symbol_table.find_symbol(
1✔
3580
        maybe_import_or_require_sym
3581
    )
3582
    if maybe_import_or_require_entry is not None:
1✔
3583
        if maybe_import_or_require_entry.context == LocalType.IMPORT:
1✔
3584
            ctx.symbol_table.mark_used(maybe_import_or_require_sym)
1✔
3585
            return MaybeHostForm(
1✔
3586
                form=form,
3587
                class_=munge(form.ns),
3588
                field=munge(form.name),
3589
                target=None,
3590
                env=ctx.get_node_env(pos=ctx.syntax_position),
3591
            )
3592

3593
    # Static and class methods on types in the current namespace can be referred
3594
    # to as `Type/static-method`. In these cases, we will try to resolve the
3595
    # namespace portion of the symbol as a Var within the current namespace.
3596
    maybe_type_or_class = current_ns.find(sym.symbol(form.ns))
1✔
3597
    if maybe_type_or_class is not None:
1✔
3598
        safe_name = munge(form.name)
1✔
3599
        member = getattr(maybe_type_or_class.value, safe_name, None)
1✔
3600

3601
        if member is None:
1✔
3602
            raise ctx.AnalyzerException(
1✔
3603
                f"unable to resolve static or class member '{form}' in this context",
3604
                form=form,
3605
            )
3606

3607
        return HostField(
1✔
3608
            form=form,
3609
            field=safe_name,
3610
            target=VarRef(
3611
                form=form,
3612
                var=maybe_type_or_class,
3613
                env=ctx.get_node_env(pos=ctx.syntax_position),
3614
            ),
3615
            is_assignable=False,
3616
            env=ctx.get_node_env(pos=ctx.syntax_position),
3617
        )
3618

3619
    raise ctx.AnalyzerException(
1✔
3620
        f"unable to resolve symbol '{form}' in this context", form=form
3621
    )
3622

3623

3624
def __resolve_bare_symbol(
1✔
3625
    ctx: AnalyzerContext, form: sym.Symbol
3626
) -> Union[Const, MaybeClass, VarRef]:
3627
    """Resolve a non-namespaced symbol into a Python name or a local
3628
    Basilisp Var."""
3629
    assert form.ns is None
1✔
3630

3631
    # Look up the symbol in the namespace mapping of the current namespace.
3632
    current_ns = ctx.current_ns
1✔
3633
    v = current_ns.find(form)
1✔
3634
    if v is not None:
1✔
3635
        return VarRef(
1✔
3636
            form=form,
3637
            var=v,
3638
            env=ctx.get_node_env(pos=ctx.syntax_position),
3639
        )
3640

3641
    if "." in form.name:
1✔
3642
        raise ctx.AnalyzerException(
1✔
3643
            "symbol names may not contain the '.' operator", form=form
3644
        )
3645

3646
    munged = munge(form.name, allow_builtins=True)
1✔
3647
    if munged in vars(builtins):
1✔
3648
        return MaybeClass(
1✔
3649
            form=form,
3650
            class_=munged,
3651
            target=vars(builtins)[munged],
3652
            env=ctx.get_node_env(pos=ctx.syntax_position),
3653
        )
3654

3655
    # Allow users to resolve imported module names directly
3656
    maybe_import = current_ns.get_import(form)
1✔
3657
    if maybe_import is not None:
1✔
3658
        return MaybeClass(
1✔
3659
            form=form,
3660
            class_=munge(form.name),
3661
            target=maybe_import,
3662
            env=ctx.get_node_env(pos=ctx.syntax_position),
3663
        )
3664

3665
    if ctx.should_allow_unresolved_symbols:
1✔
3666
        return _const_node(form, ctx)
1✔
3667

3668
    assert munged not in vars(current_ns.module)
1✔
3669
    raise ctx.AnalyzerException(
1✔
3670
        f"unable to resolve symbol '{form}' in this context", form=form
3671
    )
3672

3673

3674
def _resolve_sym(
1✔
3675
    ctx: AnalyzerContext, form: sym.Symbol
3676
) -> Union[Const, HostField, MaybeClass, MaybeHostForm, VarRef]:
3677
    """Resolve a Basilisp symbol as a Var or Python name."""
3678
    # Support special class-name syntax to instantiate new classes
3679
    #   (Classname. *args)
3680
    #   (aliased.Classname. *args)
3681
    #   (fully.qualified.Classname. *args)
3682
    if (
1✔
3683
        form.ns is None
3684
        and form.name.endswith(".")
3685
        and form.name != _DOUBLE_DOT_MACRO_NAME
3686
    ):
3687
        try:
1✔
3688
            ns, name = form.name[:-1].rsplit(".", maxsplit=1)
1✔
3689
            form = sym.symbol(name, ns=ns)
1✔
3690
        except ValueError:
1✔
3691
            form = sym.symbol(form.name[:-1])
1✔
3692

3693
    if form.ns is not None:
1✔
3694
        return __resolve_namespaced_symbol(ctx, form)
1✔
3695
    else:
3696
        return __resolve_bare_symbol(ctx, form)
1✔
3697

3698

3699
@_analyze_form.register(sym.Symbol)
1✔
3700
@_with_loc
1✔
3701
def _symbol_node(
1✔
3702
    form: sym.Symbol, ctx: AnalyzerContext
3703
) -> Union[Const, HostField, Local, MaybeClass, MaybeHostForm, VarRef]:
3704
    if ctx.is_quoted:
1✔
3705
        return _const_node(form, ctx)
1✔
3706

3707
    sym_entry = ctx.symbol_table.find_symbol(form)
1✔
3708
    if sym_entry is not None:
1✔
3709
        ctx.symbol_table.mark_used(form)
1✔
3710
        return Local(
1✔
3711
            form=form,
3712
            name=form.name,
3713
            local=sym_entry.context,
3714
            is_assignable=sym_entry.binding.is_assignable,
3715
            env=ctx.get_node_env(pos=ctx.syntax_position),
3716
        )
3717

3718
    return _resolve_sym(ctx, form)
1✔
3719

3720

3721
@_analyze_form.register(dict)
1✔
3722
@_with_loc
1✔
3723
def _py_dict_node(form: dict, ctx: AnalyzerContext) -> Union[Const, PyDict]:
1✔
3724
    if ctx.is_quoted:
1✔
3725
        return _const_node(form, ctx)
1✔
3726

3727
    keys, vals = [], []
1✔
3728
    for k, v in form.items():
1✔
3729
        keys.append(_analyze_form(k, ctx))
1✔
3730
        vals.append(_analyze_form(v, ctx))
1✔
3731

3732
    return PyDict(
1✔
3733
        form=form,
3734
        keys=vec.vector(keys),
3735
        vals=vec.vector(vals),
3736
        env=ctx.get_node_env(pos=ctx.syntax_position),
3737
    )
3738

3739

3740
@_analyze_form.register(list)
1✔
3741
@_with_loc
1✔
3742
def _py_list_node(form: list, ctx: AnalyzerContext) -> Union[Const, PyList]:
1✔
3743
    if ctx.is_quoted:
1✔
3744
        return _const_node(form, ctx)
1✔
3745
    return PyList(
1✔
3746
        form=form,
3747
        items=vec.vector(map(lambda form: _analyze_form(form, ctx), form)),
3748
        env=ctx.get_node_env(pos=ctx.syntax_position),
3749
    )
3750

3751

3752
@_analyze_form.register(set)
1✔
3753
@_with_loc
1✔
3754
def _py_set_node(form: set, ctx: AnalyzerContext) -> Union[Const, PySet]:
1✔
3755
    if ctx.is_quoted:
1✔
3756
        return _const_node(form, ctx)
1✔
3757
    return PySet(
1✔
3758
        form=form,
3759
        items=vec.vector(map(lambda form: _analyze_form(form, ctx), form)),
3760
        env=ctx.get_node_env(pos=ctx.syntax_position),
3761
    )
3762

3763

3764
@_analyze_form.register(tuple)
1✔
3765
@_with_loc
1✔
3766
def _py_tuple_node(form: tuple, ctx: AnalyzerContext) -> Union[Const, PyTuple]:
1✔
3767
    if ctx.is_quoted:
1✔
3768
        return _const_node(form, ctx)
1✔
3769
    return PyTuple(
1✔
3770
        form=form,
3771
        items=vec.vector(map(lambda form: _analyze_form(form, ctx), form)),
3772
        env=ctx.get_node_env(pos=ctx.syntax_position),
3773
    )
3774

3775

3776
@_with_meta
1✔
3777
def _map_node(form: lmap.PersistentMap, ctx: AnalyzerContext) -> MapNode:
1✔
3778
    keys, vals = [], []
1✔
3779
    for k, v in form.items():
1✔
3780
        keys.append(_analyze_form(k, ctx))
1✔
3781
        vals.append(_analyze_form(v, ctx))
1✔
3782

3783
    return MapNode(
1✔
3784
        form=form,
3785
        keys=vec.vector(keys),
3786
        vals=vec.vector(vals),
3787
        env=ctx.get_node_env(pos=ctx.syntax_position),
3788
    )
3789

3790

3791
@_analyze_form.register(lmap.PersistentMap)
1✔
3792
@_with_loc
1✔
3793
def _map_node_or_quoted(
1✔
3794
    form: lmap.PersistentMap, ctx: AnalyzerContext
3795
) -> Union[Const, MapNode]:
3796
    if ctx.is_quoted:
1✔
3797
        return _const_node(form, ctx)
1✔
3798
    return _map_node(form, ctx)
1✔
3799

3800

3801
@_with_meta
1✔
3802
def _queue_node(form: lqueue.PersistentQueue, ctx: AnalyzerContext) -> QueueNode:
1✔
3803
    return QueueNode(
1✔
3804
        form=form,
3805
        items=vec.vector(map(lambda form: _analyze_form(form, ctx), form)),
3806
        env=ctx.get_node_env(pos=ctx.syntax_position),
3807
    )
3808

3809

3810
@_analyze_form.register(lqueue.PersistentQueue)
1✔
3811
@_with_loc
1✔
3812
def _queue_node_or_quoted(
1✔
3813
    form: lqueue.PersistentQueue, ctx: AnalyzerContext
3814
) -> Union[Const, QueueNode]:
3815
    if ctx.is_quoted:
1✔
3816
        return _const_node(form, ctx)
1✔
3817
    return _queue_node(form, ctx)
1✔
3818

3819

3820
@_with_meta
1✔
3821
def _set_node(form: lset.PersistentSet, ctx: AnalyzerContext) -> SetNode:
1✔
3822
    return SetNode(
1✔
3823
        form=form,
3824
        items=vec.vector(map(lambda form: _analyze_form(form, ctx), form)),
3825
        env=ctx.get_node_env(pos=ctx.syntax_position),
3826
    )
3827

3828

3829
@_analyze_form.register(lset.PersistentSet)
1✔
3830
@_with_loc
1✔
3831
def _set_node_or_quoted(
1✔
3832
    form: lset.PersistentSet, ctx: AnalyzerContext
3833
) -> Union[Const, SetNode]:
3834
    if ctx.is_quoted:
1✔
3835
        return _const_node(form, ctx)
1✔
3836
    return _set_node(form, ctx)
1✔
3837

3838

3839
@_with_meta
1✔
3840
def _vector_node(form: vec.PersistentVector, ctx: AnalyzerContext) -> VectorNode:
1✔
3841
    return VectorNode(
1✔
3842
        form=form,
3843
        items=vec.vector(map(lambda form: _analyze_form(form, ctx), form)),
3844
        env=ctx.get_node_env(pos=ctx.syntax_position),
3845
    )
3846

3847

3848
@_analyze_form.register(vec.PersistentVector)
1✔
3849
@_with_loc
1✔
3850
def _vector_node_or_quoted(
1✔
3851
    form: vec.PersistentVector, ctx: AnalyzerContext
3852
) -> Union[Const, VectorNode]:
3853
    if ctx.is_quoted:
1✔
3854
        return _const_node(form, ctx)
1✔
3855
    return _vector_node(form, ctx)
1✔
3856

3857

3858
@functools.singledispatch
1✔
3859
def _const_node_type(_: Any) -> ConstType:
1✔
UNCOV
3860
    return ConstType.UNKNOWN
×
3861

3862

3863
for tp, const_type in {
1✔
3864
    bool: ConstType.BOOL,
3865
    bytes: ConstType.BYTES,
3866
    complex: ConstType.NUMBER,
3867
    datetime: ConstType.INST,
3868
    Decimal: ConstType.DECIMAL,
3869
    dict: ConstType.PY_DICT,
3870
    float: ConstType.NUMBER,
3871
    Fraction: ConstType.FRACTION,
3872
    int: ConstType.NUMBER,
3873
    kw.Keyword: ConstType.KEYWORD,
3874
    list: ConstType.PY_LIST,
3875
    llist.PersistentList: ConstType.SEQ,
3876
    lmap.PersistentMap: ConstType.MAP,
3877
    lqueue.PersistentQueue: ConstType.QUEUE,
3878
    lset.PersistentSet: ConstType.SET,
3879
    IRecord: ConstType.RECORD,
3880
    ISeq: ConstType.SEQ,
3881
    IType: ConstType.TYPE,
3882
    type(re.compile("")): ConstType.REGEX,
3883
    set: ConstType.PY_SET,
3884
    sym.Symbol: ConstType.SYMBOL,
3885
    str: ConstType.STRING,
3886
    tuple: ConstType.PY_TUPLE,
3887
    type(None): ConstType.NIL,
3888
    uuid.UUID: ConstType.UUID,
3889
    vec.PersistentVector: ConstType.VECTOR,
3890
}.items():
3891
    _const_node_type.register(tp, lambda _, default=const_type: default)
1✔
3892

3893

3894
@_analyze_form.register(bool)
1✔
3895
@_analyze_form.register(bytes)
1✔
3896
@_analyze_form.register(complex)
1✔
3897
@_analyze_form.register(datetime)
1✔
3898
@_analyze_form.register(Decimal)
1✔
3899
@_analyze_form.register(float)
1✔
3900
@_analyze_form.register(Fraction)
1✔
3901
@_analyze_form.register(int)
1✔
3902
@_analyze_form.register(IRecord)
1✔
3903
@_analyze_form.register(IType)
1✔
3904
@_analyze_form.register(kw.Keyword)
1✔
3905
@_analyze_form.register(type(re.compile(r"")))
1✔
3906
@_analyze_form.register(str)
1✔
3907
@_analyze_form.register(type(None))
1✔
3908
@_analyze_form.register(uuid.UUID)
1✔
3909
@_with_loc
1✔
3910
def _const_node(form: ReaderForm, ctx: AnalyzerContext) -> Const:
1✔
3911
    assert (
1✔
3912
        (
3913
            ctx.is_quoted
3914
            and isinstance(
3915
                form,
3916
                (
3917
                    sym.Symbol,
3918
                    vec.PersistentVector,
3919
                    llist.PersistentList,
3920
                    lmap.PersistentMap,
3921
                    lqueue.PersistentQueue,
3922
                    lset.PersistentSet,
3923
                    ISeq,
3924
                ),
3925
            )
3926
        )
3927
        or (ctx.should_allow_unresolved_symbols and isinstance(form, sym.Symbol))
3928
        or (isinstance(form, (llist.PersistentList, ISeq)) and form.is_empty)
3929
        or isinstance(
3930
            form,
3931
            (
3932
                bool,
3933
                bytes,
3934
                complex,
3935
                datetime,
3936
                Decimal,
3937
                dict,
3938
                float,
3939
                Fraction,
3940
                int,
3941
                IRecord,
3942
                IType,
3943
                kw.Keyword,
3944
                list,
3945
                Pattern,
3946
                set,
3947
                str,
3948
                tuple,
3949
                type(None),
3950
                uuid.UUID,
3951
            ),
3952
        )
3953
    ), "Constant nodes must be composed of constant values"
3954

3955
    node_type = _const_node_type(form)
1✔
3956
    assert node_type != ConstType.UNKNOWN, "Only allow known constant types"
1✔
3957

3958
    descriptor = Const(
1✔
3959
        form=form,
3960
        is_literal=True,
3961
        type=node_type,
3962
        val=form,
3963
        env=ctx.get_node_env(pos=ctx.syntax_position),
3964
    )
3965

3966
    if hasattr(form, "meta"):
1✔
3967
        form_meta = _clean_meta(form.meta)  # type: ignore
1✔
3968
        if form_meta is not None:
1✔
3969
            meta_ast = _const_node(form_meta, ctx)
1✔
3970
            assert isinstance(meta_ast, MapNode) or (
1✔
3971
                isinstance(meta_ast, Const) and meta_ast.type == ConstType.MAP
3972
            )
3973
            return descriptor.assoc(meta=meta_ast, children=vec.v(META))
1✔
3974

3975
    return descriptor
1✔
3976

3977

3978
###################
3979
# Public Functions
3980
###################
3981

3982

3983
def analyze_form(ctx: AnalyzerContext, form: ReaderForm) -> Node:
1✔
3984
    """Take a Lisp form as an argument and produce a Basilisp syntax
3985
    tree matching the clojure.tools.analyzer AST spec."""
3986
    return _analyze_form(form, ctx).assoc(top_level=True)
1✔
3987

3988

3989
def macroexpand_1(form: ReaderForm) -> ReaderForm:
1✔
3990
    """Macroexpand form one time. Returns the macroexpanded form. The return
3991
    value may still represent a macro. Does not macroexpand child forms."""
3992
    ctx = AnalyzerContext(
1✔
3993
        "<Macroexpand>", should_macroexpand=False, allow_unresolved_symbols=True
3994
    )
3995
    maybe_macro = analyze_form(ctx, form)
1✔
3996
    if maybe_macro.op == NodeOp.INVOKE:
1✔
3997
        assert isinstance(maybe_macro, Invoke)
1✔
3998

3999
        fn = maybe_macro.fn
1✔
4000
        if fn.op == NodeOp.VAR and isinstance(fn, VarRef):
1✔
4001
            if _is_macro(fn.var):
1✔
4002
                assert isinstance(form, ISeq)
1✔
4003
                macro_env = ctx.symbol_table.as_env_map()
1✔
4004
                return fn.var.value(macro_env, form, *form.rest)
1✔
4005
    return maybe_macro.form
1✔
4006

4007

4008
def macroexpand(form: ReaderForm) -> ReaderForm:
1✔
4009
    """Repeatedly macroexpand form as by macroexpand-1 until form no longer
4010
    represents a macro. Returns the expanded form. Does not macroexpand child
4011
    forms."""
4012
    return analyze_form(
1✔
4013
        AnalyzerContext("<Macroexpand>", allow_unresolved_symbols=True), form
4014
    ).form
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc