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

basilisp-lang / basilisp / 20628625851

31 Dec 2025 10:56PM UTC coverage: 98.681% (-0.009%) from 98.69%
20628625851

Pull #1291

github

web-flow
Merge a7b3c7dfd into 9fb9364f9
Pull Request #1291: Fixes in support of the clojure-test-suite

1061 of 1070 branches covered (99.16%)

Branch coverage included in aggregate %.

8 of 10 new or added lines in 1 file covered. (80.0%)

22 existing lines in 2 files now uncovered.

9039 of 9165 relevant lines covered (98.63%)

0.99 hits per line

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

97.96
/src/basilisp/lang/runtime.py
1
# pylint: disable=too-many-lines
2
import builtins
1✔
3
import collections.abc
1✔
4
import contextlib
1✔
5
import decimal
1✔
6
import functools
1✔
7
import graphlib
1✔
8
import importlib.metadata
1✔
9
import inspect
1✔
10
import itertools
1✔
11
import logging
1✔
12
import math
1✔
13
import numbers
1✔
14
import pickle  # nosec B403
1✔
15
import platform
1✔
16
import re
1✔
17
import sys
1✔
18
import threading
1✔
19
import types
1✔
20
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence, Sized
1✔
21
from fractions import Fraction
1✔
22
from typing import AbstractSet, Any, NoReturn, Optional, TypeVar, Union, cast
1✔
23

24
import attr
1✔
25

26
from basilisp.lang import keyword as kw
1✔
27
from basilisp.lang import list as llist
1✔
28
from basilisp.lang import map as lmap
1✔
29
from basilisp.lang import obj as lobj
1✔
30
from basilisp.lang import seq as lseq
1✔
31
from basilisp.lang import set as lset
1✔
32
from basilisp.lang import symbol as sym
1✔
33
from basilisp.lang import vector as vec
1✔
34
from basilisp.lang.atom import Atom
1✔
35
from basilisp.lang.interfaces import (
1✔
36
    IAssociative,
37
    IBlockingDeref,
38
    IDeref,
39
    ILookup,
40
    IMapEntry,
41
    IPersistentCollection,
42
    IPersistentList,
43
    IPersistentMap,
44
    IPersistentSet,
45
    IPersistentStack,
46
    IPersistentVector,
47
    IReduce,
48
    ISeq,
49
    ISeqable,
50
    ITransientAssociative,
51
    ITransientSet,
52
    ReduceFunction,
53
)
54
from basilisp.lang.reduced import Reduced
1✔
55
from basilisp.lang.reference import RefBase, ReferenceBase
1✔
56
from basilisp.lang.typing import BasilispFunction, CompilerOpts, LispNumber
1✔
57
from basilisp.lang.util import OBJECT_DUNDER_METHODS, demunge, is_abstract, munge
1✔
58
from basilisp.util import Maybe
1✔
59

60
logger = logging.getLogger(__name__)
1✔
61

62
# Public constants
63
CORE_NS = "basilisp.core"
1✔
64
CORE_NS_SYM = sym.symbol(CORE_NS)
1✔
65
NS_VAR_NAME = "*ns*"
1✔
66
NS_VAR_SYM = sym.symbol(NS_VAR_NAME, ns=CORE_NS)
1✔
67
IMPORT_MODULE_VAR_NAME = "*import-module*"
1✔
68
IMPORT_MODULE_VAR_SYM = sym.symbol(IMPORT_MODULE_VAR_NAME, ns=CORE_NS)
1✔
69
NS_VAR_NS = CORE_NS
1✔
70
REPL_DEFAULT_NS = "basilisp.user"
1✔
71
SUPPORTED_PYTHON_VERSIONS = frozenset({(3, 10), (3, 11), (3, 12), (3, 13), (3, 14)})
1✔
72
BASILISP_VERSION_STRING = importlib.metadata.version("basilisp")
1✔
73
BASILISP_VERSION = vec.vector(
1✔
74
    (int(s) if s.isdigit() else s) for s in BASILISP_VERSION_STRING.split(".")
75
)
76

77
# Public basilisp.core symbol names
78
COMPILER_OPTIONS_VAR_NAME = "*compiler-options*"
1✔
79
COMMAND_LINE_ARGS_VAR_NAME = "*command-line-args*"
1✔
80
DEFAULT_READER_FEATURES_VAR_NAME = "*default-reader-features*"
1✔
81
GENERATED_PYTHON_VAR_NAME = "*generated-python*"
1✔
82
PRINT_GENERATED_PY_VAR_NAME = "*print-generated-python*"
1✔
83
MAIN_NS_VAR_NAME = "*main-ns*"
1✔
84
PRINT_DUP_VAR_NAME = "*print-dup*"
1✔
85
PRINT_LENGTH_VAR_NAME = "*print-length*"
1✔
86
PRINT_LEVEL_VAR_NAME = "*print-level*"
1✔
87
PRINT_META_VAR_NAME = "*print-meta*"
1✔
88
PRINT_NAMESPACE_MAPS_VAR_NAME = "*print-namespace-maps*"
1✔
89
PRINT_READABLY_VAR_NAME = "*print-readably*"
1✔
90
PYTHON_VERSION_VAR_NAME = "*python-version*"
1✔
91
BASILISP_VERSION_VAR_NAME = "*basilisp-version*"
1✔
92

93
# Common meta keys
94
_DOC_META_KEY = kw.keyword("doc")
1✔
95
_DYNAMIC_META_KEY = kw.keyword("dynamic")
1✔
96
_PRIVATE_META_KEY = kw.keyword("private")
1✔
97
_REDEF_META_KEY = kw.keyword("redef")
1✔
98

99
# Special form values, used for resolving Vars
100
_AWAIT = sym.symbol("await")
1✔
101
_CATCH = sym.symbol("catch")
1✔
102
_DEF = sym.symbol("def")
1✔
103
_DEFTYPE = sym.symbol("deftype*")
1✔
104
_DO = sym.symbol("do")
1✔
105
_FINALLY = sym.symbol("finally")
1✔
106
_FN = sym.symbol("fn*")
1✔
107
_IF = sym.symbol("if")
1✔
108
_IMPORT = sym.symbol("import*")
1✔
109
_INTEROP_CALL = sym.symbol(".")
1✔
110
_INTEROP_PROP = sym.symbol(".-")
1✔
111
_LET = sym.symbol("let*")
1✔
112
_LETFN = sym.symbol("letfn*")
1✔
113
_LOOP = sym.symbol("loop*")
1✔
114
_QUOTE = sym.symbol("quote")
1✔
115
_REIFY = sym.symbol("reify*")
1✔
116
_RECUR = sym.symbol("recur")
1✔
117
_REQUIRE = sym.symbol("require*")
1✔
118
_SET_BANG = sym.symbol("set!")
1✔
119
_THROW = sym.symbol("throw")
1✔
120
_TRY = sym.symbol("try")
1✔
121
_VAR = sym.symbol("var")
1✔
122
_YIELD = sym.symbol("yield")
1✔
123
_SPECIAL_FORMS = lset.s(
1✔
124
    _AWAIT,
125
    _CATCH,
126
    _DEF,
127
    _DEFTYPE,
128
    _DO,
129
    _FINALLY,
130
    _FN,
131
    _IF,
132
    _IMPORT,
133
    _INTEROP_CALL,
134
    _INTEROP_PROP,
135
    _LET,
136
    _LETFN,
137
    _LOOP,
138
    _QUOTE,
139
    _RECUR,
140
    _REIFY,
141
    _REQUIRE,
142
    _SET_BANG,
143
    _THROW,
144
    _TRY,
145
    _VAR,
146
    _YIELD,
147
)
148

149

150
# Reader Conditional default features
151
def _supported_python_versions_features() -> Iterable[kw.Keyword]:
1✔
152
    """Yield successive reader features corresponding to the various Python
153
    `major`.`minor` versions the current Python VM corresponds to amongst the
154
    set of supported Python versions.
155

156
    For example, for Python 3.6, we would emit:
157
     - :lpy36  - to exactly match Basilisp running on Python 3.6
158
     - :lpy36+ - to match Basilisp running on Python 3.6 and later versions
159
     - :lpy36- - to match Basilisp running on Python 3.6 and earlier versions
160
     - :lpy37- - to match Basilisp running on Python 3.7 and earlier versions
161
     - :lpy38- - to match Basilisp running on Python 3.8 and earlier versions"""
162
    feature_kw = lambda major, minor, suffix="": kw.keyword(
1✔
163
        f"lpy{major}{minor}{suffix}"
164
    )
165

166
    yield feature_kw(sys.version_info.major, sys.version_info.minor)
1✔
167

168
    current = (sys.version_info.major, sys.version_info.minor)
1✔
169
    for version in SUPPORTED_PYTHON_VERSIONS:
1✔
170
        if current <= version:
1✔
171
            yield feature_kw(version[0], version[1], suffix="-")
1✔
172
        if current >= version:
1✔
173
            yield feature_kw(version[0], version[1], suffix="+")
1✔
174

175

176
READER_COND_BASILISP_FEATURE_KW = kw.keyword("lpy")
1✔
177
READER_COND_DEFAULT_FEATURE_KW = kw.keyword("default")
1✔
178
READER_COND_PLATFORM = kw.keyword(platform.system().lower())
1✔
179
READER_COND_DEFAULT_FEATURE_SET = lset.s(
1✔
180
    READER_COND_BASILISP_FEATURE_KW,
181
    READER_COND_DEFAULT_FEATURE_KW,
182
    READER_COND_PLATFORM,
183
    *_supported_python_versions_features(),
184
)
185

186
CompletionMatcher = Callable[[tuple[sym.Symbol, Any]], bool]
1✔
187
CompletionTrimmer = Callable[[tuple[sym.Symbol, Any]], str]
1✔
188

189

190
class BasilispModule(types.ModuleType):
1✔
191
    __basilisp_namespace__: "Namespace"
1✔
192
    __basilisp_bootstrapped__: bool = False
1✔
193

194

195
# pylint: disable=attribute-defined-outside-init
196
def _new_module(name: str, doc=None) -> BasilispModule:
1✔
197
    """Create a new empty Basilisp Python module.
198
    Modules are created for each Namespace when it is created."""
199
    mod = BasilispModule(name, doc=doc)
1✔
200
    mod.__loader__ = None
1✔
201
    mod.__package__ = None
1✔
202
    mod.__spec__ = None
1✔
203
    mod.__basilisp_bootstrapped__ = False
1✔
204
    return mod
1✔
205

206

207
class RuntimeException(Exception):
1✔
208
    pass
1✔
209

210

211
class _VarBindings(threading.local):
1✔
212
    def __init__(self):
1✔
213
        self.bindings: list = []
1✔
214

215

216
class Unbound:
1✔
217
    __slots__ = ("var",)
1✔
218

219
    def __init__(self, v: "Var"):
1✔
220
        self.var = v
1✔
221

222
    def __repr__(self):  # pragma: no cover
223
        return f"Unbound(var={self.var})"
224

225
    def __eq__(self, other):
1✔
226
        return self is other or (isinstance(other, Unbound) and self.var == other.var)
1✔
227

228

229
class Var(RefBase):
1✔
230
    __slots__ = (
1✔
231
        "_name",
232
        "_ns",
233
        "_root",
234
        "_dynamic",
235
        "_is_bound",
236
        "_tl",
237
        "_meta",
238
        "_lock",
239
        "_watches",
240
        "_validator",
241
    )
242

243
    def __init__(
1✔
244
        self,
245
        ns: "Namespace",
246
        name: sym.Symbol,
247
        dynamic: bool = False,
248
        meta: IPersistentMap | None = None,
249
    ) -> None:
250
        self._ns = ns
1✔
251
        self._name = name
1✔
252
        self._root = Unbound(self)
1✔
253
        self._dynamic = dynamic
1✔
254
        self._is_bound = False
1✔
255
        self._tl = None
1✔
256
        self._meta = meta
1✔
257
        self._lock = threading.RLock()
1✔
258
        self._watches = lmap.EMPTY
1✔
259
        self._validator = None
1✔
260

261
        if dynamic:
1✔
262
            self._tl = _VarBindings()
1✔
263

264
            # If this var was created with the dynamic keyword argument, then the
265
            # Var metadata should also specify that the Var is dynamic.
266
            if isinstance(self._meta, lmap.PersistentMap):
1✔
267
                if not self._meta.val_at(_DYNAMIC_META_KEY):
1✔
268
                    self._meta = self._meta.assoc(_DYNAMIC_META_KEY, True)
1✔
269
            else:
270
                self._meta = lmap.map({_DYNAMIC_META_KEY: True})
1✔
271

272
    def __repr__(self):
273
        return f"#'{self.ns.name}/{self.name}"
274

275
    def __call__(self, *args, **kwargs):
1✔
276
        return self.value(*args, *kwargs)  # pylint: disable=not-callable
1✔
277

278
    @property
1✔
279
    def ns(self) -> "Namespace":
1✔
280
        return self._ns
1✔
281

282
    @property
1✔
283
    def name(self) -> sym.Symbol:
1✔
284
        return self._name
1✔
285

286
    @property
1✔
287
    def dynamic(self) -> bool:
1✔
288
        return self._dynamic
1✔
289

290
    def set_dynamic(self, dynamic: bool) -> None:
1✔
291
        with self._lock:
1✔
292
            if dynamic == self._dynamic:
1✔
293
                return
1✔
294

295
            self._dynamic = dynamic
1✔
296
            self._tl = _VarBindings() if dynamic else None
1✔
297

298
    @property
1✔
299
    def is_private(self) -> bool | None:
1✔
300
        if self._meta is not None:
1✔
301
            return self._meta.val_at(_PRIVATE_META_KEY)
1✔
302
        return False
1✔
303

304
    @property
1✔
305
    def is_bound(self) -> bool:
1✔
306
        return self._is_bound or self.is_thread_bound
1✔
307

308
    def _set_root(self, newval) -> None:
1✔
309
        # Private setter which does not include lock so it can be used
310
        # by other properties and methods which already manage the lock.
311
        oldval = self._root
1✔
312
        self._validate(newval)
1✔
313
        self._is_bound = True
1✔
314
        self._root = newval
1✔
315
        self._notify_watches(oldval, newval)
1✔
316
        return newval
1✔
317

318
    @property
1✔
319
    def root(self):
1✔
320
        with self._lock:
1✔
321
            return self._root
1✔
322

323
    def bind_root(self, val) -> None:
1✔
324
        """Atomically update the root binding of this Var to val."""
325
        with self._lock:
1✔
326
            self._set_root(val)
1✔
327

328
    def alter_root(self, f, *args) -> None:
1✔
329
        """Atomically alter the root binding of this Var to the result of calling
330
        f with the existing root value and any additional arguments. Returns the
331
        new value."""
332
        with self._lock:
1✔
333
            return self._set_root(f(self._root, *args))
1✔
334

335
    def push_bindings(self, val):
1✔
336
        if not self._dynamic or self._tl is None:
1✔
337
            raise RuntimeException("Can only push bindings to dynamic Vars")
1✔
338
        self._validate(val)
1✔
339
        self._tl.bindings.append(val)
1✔
340

341
    def pop_bindings(self):
1✔
342
        if not self._dynamic or self._tl is None:
1✔
343
            raise RuntimeException("Can only pop bindings from dynamic Vars")
1✔
344
        return self._tl.bindings.pop()
1✔
345

346
    def deref(self):
1✔
347
        return self.value
1✔
348

349
    @property
1✔
350
    def is_thread_bound(self):
1✔
351
        return bool(self._dynamic and self._tl and self._tl.bindings)
1✔
352

353
    @property
1✔
354
    def value(self):
1✔
355
        """Return the current value of the Var visible in the current thread.
356

357
        For non-dynamic Vars, this will just be the root. For dynamic Vars, this will
358
        be any thread-local binding if one is defined. Otherwise, the root value."""
359
        with self._lock:
1✔
360
            if self._dynamic:
1✔
361
                assert self._tl is not None
1✔
362
                if len(self._tl.bindings) > 0:
1✔
363
                    return self._tl.bindings[-1]
1✔
364
            return self._root
1✔
365

366
    def set_value(self, v) -> None:
1✔
367
        """Set the current value of the Var.
368

369
        If the Var is not dynamic, this is equivalent to binding the root value. If the
370
        Var is dynamic, this will set the thread-local bindings for the Var."""
371
        with self._lock:
1✔
372
            if self._dynamic:
1✔
373
                assert self._tl is not None
1✔
374
                self._validate(v)
1✔
375
                if len(self._tl.bindings) > 0:
1✔
376
                    self._tl.bindings[-1] = v
1✔
377
                else:
378
                    self.push_bindings(v)
1✔
379
                return
1✔
380
            self._set_root(v)
1✔
381

382
    __UNBOUND_SENTINEL = object()
1✔
383

384
    @classmethod
1✔
385
    def intern(  # pylint: disable=too-many-arguments
1✔
386
        cls,
387
        ns: Union["Namespace", sym.Symbol],
388
        name: sym.Symbol,
389
        val,
390
        dynamic: bool = False,
391
        meta: IPersistentMap | None = None,
392
    ) -> "Var":
393
        """Intern the value bound to the symbol `name` in namespace `ns`.
394

395
        If the Var already exists, it will have its root value updated to `val`,
396
        its meta will be reset to match the provided `meta`, and the dynamic flag
397
        will be updated to match the provided `dynamic` argument."""
398
        if isinstance(ns, sym.Symbol):
1✔
399
            ns = Namespace.get_or_create(ns)
1✔
400
        new_var = cls(ns, name, dynamic=dynamic, meta=meta)
1✔
401
        var = ns.intern(name, new_var)
1✔
402
        if val is not cls.__UNBOUND_SENTINEL:
1✔
403
            var.bind_root(val)
1✔
404
        # Namespace.intern will return an existing interned Var for the same name
405
        # if one exists. We only want to set the meta and/or dynamic flag if the
406
        # Var is not new.
407
        if var is not new_var:
1✔
408
            var.reset_meta(meta)
1✔
409
            var.set_dynamic(dynamic)
1✔
410
        return var
1✔
411

412
    @classmethod
1✔
413
    def intern_unbound(
1✔
414
        cls,
415
        ns: Union["Namespace", sym.Symbol],
416
        name: sym.Symbol,
417
        dynamic: bool = False,
418
        meta: IPersistentMap | None = None,
419
    ) -> "Var":
420
        """Create a new unbound `Var` instance to the symbol `name` in namespace `ns`."""
421
        return cls.intern(ns, name, cls.__UNBOUND_SENTINEL, dynamic=dynamic, meta=meta)
1✔
422

423
    @staticmethod
1✔
424
    def find_in_ns(
1✔
425
        ns_or_sym: Union["Namespace", sym.Symbol], name_sym: sym.Symbol
426
    ) -> "Optional[Var]":
427
        """Return the value current bound to the name `name_sym` in the namespace
428
        specified by `ns_sym`."""
429
        ns = (
1✔
430
            Namespace.get(ns_or_sym) if isinstance(ns_or_sym, sym.Symbol) else ns_or_sym
431
        )
432
        if ns is not None:
1✔
433
            return ns.find(name_sym)
1✔
434
        return None
1✔
435

436
    @classmethod
1✔
437
    def find(cls, ns_qualified_sym: sym.Symbol) -> "Optional[Var]":
1✔
438
        """Return the value currently bound to the name in the namespace specified
439
        by `ns_qualified_sym`."""
440
        ns = Maybe(ns_qualified_sym.ns).or_else_raise(
1✔
441
            lambda: ValueError(
442
                f"Namespace must be specified in Symbol {ns_qualified_sym}"
443
            )
444
        )
445
        ns_sym = sym.symbol(ns)
1✔
446
        name_sym = sym.symbol(ns_qualified_sym.name)
1✔
447
        return cls.find_in_ns(ns_sym, name_sym)
1✔
448

449
    @classmethod
1✔
450
    def find_safe(cls, ns_qualified_sym: sym.Symbol) -> "Var":
1✔
451
        """Return the Var currently bound to the name in the namespace specified
452
        by `ns_qualified_sym`. If no Var is bound to that name, raise an exception.
453

454
        This is a utility method to return useful debugging information when code
455
        refers to an invalid symbol at runtime."""
456
        v = cls.find(ns_qualified_sym)
1✔
457
        if v is None:
1✔
458
            raise RuntimeException(
1✔
459
                f"Unable to resolve symbol {ns_qualified_sym} in this context"
460
            )
461
        return v
1✔
462

463

464
Frame = IPersistentSet[Var]
1✔
465
FrameStack = IPersistentStack[Frame]
1✔
466

467

468
class _ThreadBindings(threading.local):
1✔
469
    def __init__(self):
1✔
470
        self._bindings: FrameStack = vec.EMPTY
1✔
471

472
    def get_bindings(self) -> FrameStack:
1✔
473
        return self._bindings
1✔
474

475
    def push_bindings(self, frame: Frame) -> None:
1✔
476
        self._bindings = self._bindings.cons(frame)
1✔
477

478
    def pop_bindings(self) -> Frame:
1✔
479
        frame = self._bindings.peek()
1✔
480
        self._bindings = self._bindings.pop()
1✔
481
        assert frame is not None
1✔
482
        return frame
1✔
483

484

485
_THREAD_BINDINGS = _ThreadBindings()
1✔
486

487

488
@attr.frozen
1✔
489
class ImportRefer:
1✔
490
    module_name: sym.Symbol
1✔
491
    value: Any
1✔
492

493

494
AliasMap = lmap.PersistentMap[sym.Symbol, sym.Symbol]
1✔
495
ImportReferMap = lmap.PersistentMap[sym.Symbol, Any]
1✔
496
Module = Union[BasilispModule, types.ModuleType]
1✔
497
ModuleMap = lmap.PersistentMap[sym.Symbol, Module]
1✔
498
NamespaceMap = lmap.PersistentMap[sym.Symbol, "Namespace"]
1✔
499
VarMap = lmap.PersistentMap[sym.Symbol, Var]
1✔
500

501

502
_PRIVATE_NAME_PATTERN = re.compile(r"(_\w*|__\w+__)")
1✔
503

504

505
class Namespace(ReferenceBase):
1✔
506
    """Namespaces serve as organizational units in Basilisp code, just as they do in
507
    Clojure code.
508

509
    Vars are mutable containers for functions and data which may be interned in a
510
    namespace and referred to by a Symbol.
511

512
    Namespaces additionally may have aliases to other namespaces, so code organized in
513
    one namespace may conveniently refer to code or data in other namespaces using that
514
    alias as the Symbol's namespace.
515

516
    Namespaces are constructed def-by-def as Basilisp reads in each form in a file
517
    (which will typically declare a namespace at the top).
518

519
    Namespaces have the following fields of interest:
520

521
    - `aliases` is a mapping between a symbolic alias and another Namespace. The fully
522
      qualified name of a namespace is also an alias for itself.
523

524
    - `imports` is a mapping of names to Python modules imported into the current
525
      namespace.
526

527
    - `import_aliases` is a mapping of aliases for Python modules to the true module
528
      name.
529

530
    - `interns` is a mapping between a symbolic name and a Var. The Var may point to
531
      code, data, or nothing, if it is unbound. Vars in `interns` are interned in
532
      _this_ namespace.
533

534
    - `refers` is a mapping between a symbolic name and a Var. Vars in `refers` are
535
      interned in another namespace and are only referred to without an alias in
536
      this namespace.
537
    """
538

539
    # If this set is updated, be sure to update the following two locations:
540
    # - basilisp.lang.compiler.generator._MODULE_ALIASES
541
    # - the `namespace_imports` section in the documentation
542
    DEFAULT_IMPORTS = lset.set(
1✔
543
        map(
544
            sym.symbol,
545
            [
546
                "attr",
547
                "builtins",
548
                "functools",
549
                "io",
550
                "importlib",
551
                "operator",
552
                "sys",
553
                "basilisp.lang.atom",
554
                "basilisp.lang.compiler",
555
                "basilisp.lang.delay",
556
                "basilisp.lang.exception",
557
                "basilisp.lang.futures",
558
                "basilisp.lang.interfaces",
559
                "basilisp.lang.keyword",
560
                "basilisp.lang.list",
561
                "basilisp.lang.map",
562
                "basilisp.lang.multifn",
563
                "basilisp.lang.promise",
564
                "basilisp.lang.queue",
565
                "basilisp.lang.reader",
566
                "basilisp.lang.reduced",
567
                "basilisp.lang.runtime",
568
                "basilisp.lang.seq",
569
                "basilisp.lang.set",
570
                "basilisp.lang.symbol",
571
                "basilisp.lang.tagged",
572
                "basilisp.lang.vector",
573
                "basilisp.lang.volatile",
574
                "basilisp.lang.util",
575
            ],
576
        )
577
    )
578

579
    _NAMESPACES: Atom[NamespaceMap] = Atom(lmap.EMPTY)
1✔
580

581
    __slots__ = (
1✔
582
        "_name",
583
        "_module",
584
        "_meta",
585
        "_lock",
586
        "_interns",
587
        "_refers",
588
        "_aliases",
589
        "_imports",
590
        "_import_aliases",
591
        "_import_refers",
592
    )
593

594
    def __init__(self, name: sym.Symbol, module: BasilispModule | None = None) -> None:
1✔
595
        self._name = name
1✔
596
        self._module = Maybe(module).or_else(lambda: _new_module(name.as_python_sym()))
1✔
597

598
        self._meta: IPersistentMap | None = None
1✔
599
        self._lock = threading.RLock()
1✔
600

601
        self._aliases: NamespaceMap = lmap.EMPTY
1✔
602
        self._imports: ModuleMap = lmap.map(
1✔
603
            dict(
604
                map(
605
                    lambda s: (s, importlib.import_module(s.name)),
606
                    Namespace.DEFAULT_IMPORTS,
607
                )
608
            )
609
        )
610
        self._import_aliases: AliasMap = lmap.EMPTY
1✔
611
        self._import_refers: lmap.PersistentMap[sym.Symbol, ImportRefer] = lmap.EMPTY
1✔
612
        self._interns: VarMap = lmap.EMPTY
1✔
613
        self._refers: VarMap = lmap.EMPTY
1✔
614

615
    @property
1✔
616
    def name(self) -> str:
1✔
617
        return self._name.name
1✔
618

619
    @property
1✔
620
    def module(self) -> BasilispModule:
1✔
621
        return self._module
1✔
622

623
    @module.setter
1✔
624
    def module(self, m: BasilispModule):
1✔
625
        """Override the Python module for this Namespace.
626

627
        ***WARNING**
628
        This should only be done by basilisp.importer code to make sure the
629
        correct module is generated for `basilisp.core`."""
630
        self._module = m
1✔
631

632
    @property
1✔
633
    def aliases(self) -> NamespaceMap:
1✔
634
        """A mapping between a symbolic alias and another Namespace. The
635
        fully qualified name of a namespace is also an alias for itself."""
636
        with self._lock:
1✔
637
            return self._aliases
1✔
638

639
    @property
1✔
640
    def imports(self) -> ModuleMap:
1✔
641
        """A mapping of names to Python modules imported into the current
642
        namespace."""
643
        with self._lock:
1✔
644
            return self._imports
1✔
645

646
    @property
1✔
647
    def import_aliases(self) -> AliasMap:
1✔
648
        """A mapping of a symbolic alias and a Python module name."""
649
        with self._lock:
1✔
650
            return self._import_aliases
1✔
651

652
    @property
1✔
653
    def import_refers(self) -> ImportReferMap:
1✔
654
        """A mapping of a symbolic alias and a Python object from an imported module."""
655
        with self._lock:
1✔
656
            return lmap.map({name: v.value for name, v in self._import_refers.items()})
1✔
657

658
    @property
1✔
659
    def interns(self) -> VarMap:
1✔
660
        """A mapping between a symbolic name and a Var. The Var may point to
661
        code, data, or nothing, if it is unbound. Vars in `interns` are
662
        interned in _this_ namespace."""
663
        with self._lock:
1✔
664
            return self._interns
1✔
665

666
    @property
1✔
667
    def refers(self) -> VarMap:
1✔
668
        """A mapping between a symbolic name and a Var. Vars in refers are
669
        interned in another namespace and are only referred to without an
670
        alias in this namespace."""
671
        with self._lock:
1✔
672
            return self._refers
1✔
673

674
    def __repr__(self):
675
        return f"{self._name}"
676

677
    def __hash__(self):
1✔
678
        return hash(self._name)
1✔
679

680
    def _get_required_namespaces(self) -> vec.PersistentVector["Namespace"]:
1✔
681
        """Return a vector of all required namespaces (loaded via `require`, `use`,
682
        or `refer`).
683

684
        This vector will include `basilisp.core` unless the namespace was created
685
        manually without requiring it."""
686
        ts: graphlib.TopologicalSorter = graphlib.TopologicalSorter()
1✔
687

688
        def add_nodes(ns: Namespace) -> None:
1✔
689
            # basilisp.core does actually create a cycle by requiring namespaces
690
            # that require it, so we cannot add it to the topological sorter here,
691
            # or it will throw a cycle error
692
            if ns.name == CORE_NS:
1✔
693
                return
1✔
694

695
            for aliased_ns in ns.aliases.values():
1✔
696
                ts.add(ns, aliased_ns)
1✔
697
                add_nodes(aliased_ns)
1✔
698

699
            for referred_var in ns.refers.values():
1✔
700
                referred_ns = referred_var.ns
1✔
701
                ts.add(ns, referred_ns)
1✔
702
                add_nodes(referred_ns)
1✔
703

704
        add_nodes(self)
1✔
705
        return vec.vector(ts.static_order())
1✔
706

707
    def reload_all(self) -> "Namespace":
1✔
708
        """Reload all dependency namespaces as by `Namespace.reload()`."""
709
        sorted_reload_order = self._get_required_namespaces()
1✔
710
        logger.debug(f"Computed :reload-all order: {sorted_reload_order}")
1✔
711

712
        for ns in sorted_reload_order:
1✔
713
            ns.reload()
1✔
714

715
        return self
1✔
716

717
    def reload(self) -> "Namespace":
1✔
718
        """Reload code in this namespace by reloading the underlying Python module."""
719
        with self._lock:
1✔
720
            importlib.reload(self.module)
1✔
721
            return self
1✔
722

723
    def require(self, ns_name: str, *aliases: sym.Symbol) -> BasilispModule:
1✔
724
        """Require the Basilisp Namespace named by `ns_name` and add any aliases given
725
        to this Namespace.
726

727
        This method is called in code generated for the `require*` special form."""
728
        try:
1✔
729
            ns_module = importlib.import_module(munge(ns_name))
1✔
730
        except ModuleNotFoundError as e:
1✔
731
            raise ImportError(
1✔
732
                f"Basilisp namespace '{ns_name}' not found",
733
            ) from e
734
        else:
735
            assert isinstance(ns_module, BasilispModule)
1✔
736
            ns_sym = sym.symbol(ns_name)
1✔
737
            ns = self.get(ns_sym)
1✔
738
            assert ns is not None, "Namespace must exist after being required"
1✔
739
            self.add_alias(ns, ns_sym)
1✔
740
            if aliases:
1✔
741
                self.add_alias(ns, *aliases)
1✔
742
            return ns_module
1✔
743

744
    def add_alias(self, namespace: "Namespace", *aliases: sym.Symbol) -> None:
1✔
745
        """Add Symbol aliases for the given Namespace."""
746
        with self._lock:
1✔
747
            new_m = self._aliases
1✔
748
            for alias in aliases:
1✔
749
                new_m = new_m.assoc(alias, namespace)
1✔
750
            self._aliases = new_m
1✔
751

752
    def get_alias(self, alias: sym.Symbol) -> "Optional[Namespace]":
1✔
753
        """Get the Namespace aliased by Symbol or None if it does not exist."""
754
        with self._lock:
1✔
755
            return self._aliases.val_at(alias, None)
1✔
756

757
    def remove_alias(self, alias: sym.Symbol) -> None:
1✔
758
        """Remove the Namespace aliased by Symbol. Return None."""
759
        with self._lock:
1✔
760
            self._aliases = self._aliases.dissoc(alias)
1✔
761

762
    def intern(self, sym: sym.Symbol, var: Var, force: bool = False) -> Var:
1✔
763
        """Intern the Var given in this namespace mapped by the given Symbol.
764
        If the Symbol already maps to a Var, this method _will not overwrite_
765
        the existing Var mapping unless the force keyword argument is given
766
        and is True."""
767
        with self._lock:
1✔
768
            old_var = self._interns.val_at(sym, None)
1✔
769
            if old_var is None or force:
1✔
770
                self._interns = self._interns.assoc(sym, var)
1✔
771
            return self._interns.val_at(sym)
1✔
772

773
    def unmap(self, sym: sym.Symbol) -> None:
1✔
774
        with self._lock:
1✔
775
            self._interns = self._interns.dissoc(sym)
1✔
776

777
    def find(self, sym: sym.Symbol) -> Var | None:
1✔
778
        """Find Vars mapped by the given Symbol input or None if no Vars are
779
        mapped by that Symbol."""
780
        with self._lock:
1✔
781
            v = self._interns.val_at(sym, None)
1✔
782
            if v is None:
1✔
783
                return self._refers.val_at(sym, None)
1✔
784
            return v
1✔
785

786
    def add_import(
1✔
787
        self,
788
        sym: sym.Symbol,
789
        module: Module,
790
        *aliases: sym.Symbol,
791
        refers: dict[sym.Symbol, Any] | None = None,
792
    ) -> None:
793
        """Add the Symbol as an imported Symbol in this Namespace.
794

795
        If aliases are given, the aliases will be associated to the module as well.
796

797
        If a dictionary of refers is provided, add the referred names as import refers.
798
        """
799
        with self._lock:
1✔
800
            self._imports = self._imports.assoc(sym, module)
1✔
801

802
            if refers:
1✔
803
                final_refers = self._import_refers
1✔
804
                for s, v in refers.items():
1✔
805
                    # Filter out dunder names and private names
806
                    if _PRIVATE_NAME_PATTERN.fullmatch(s.name) is not None:
1✔
807
                        logger.debug(f"Ignoring import refer for {sym} member {s}")
1✔
808
                        continue
1✔
809
                    final_refers = final_refers.assoc(s, ImportRefer(sym, v))
1✔
810
                self._import_refers = final_refers
1✔
811

812
            if aliases:
1✔
813
                m = self._import_aliases
1✔
814
                for alias in aliases:
1✔
815
                    m = m.assoc(alias, sym)
1✔
816
                self._import_aliases = m
1✔
817

818
    def get_import_refer(self, sym: sym.Symbol) -> sym.Symbol | None:
1✔
819
        """Get the Python module member name referred by Symbol or None if it does not
820
        exist."""
821
        with self._lock:
1✔
822
            refer = self._import_refers.val_at(sym, None)
1✔
823
            return refer.module_name if refer is not None else None
1✔
824

825
    def get_import(self, sym: sym.Symbol) -> BasilispModule | None:
1✔
826
        """Return the module if a module named by sym has been imported into
827
        this Namespace, None otherwise.
828

829
        First try to resolve a module directly with the given name. If no module
830
        can be resolved, attempt to resolve the module using import aliases."""
831
        with self._lock:
1✔
832
            mod = self._imports.val_at(sym, None)
1✔
833
            if mod is None:
1✔
834
                alias = self._import_aliases.get(sym, None)
1✔
835
                if alias is None:
1✔
836
                    return None
1✔
837
                return self._imports.val_at(alias, None)
1✔
838
            return mod
1✔
839

840
    def add_refer(self, sym: sym.Symbol, var: Var) -> None:
1✔
841
        """Refer var in this namespace under the name sym."""
842
        if not var.is_private:
1✔
843
            with self._lock:
1✔
844
                self._refers = self._refers.assoc(sym, var)
1✔
845

846
    def get_refer(self, sym: sym.Symbol) -> Var | None:
1✔
847
        """Get the Var referred by Symbol or None if it does not exist."""
848
        with self._lock:
1✔
849
            return self._refers.val_at(sym, None)
1✔
850

851
    def refer_all(self, other_ns: "Namespace") -> None:
1✔
852
        """Refer all the Vars in the other namespace."""
853
        with self._lock:
1✔
854
            final_refers = self._refers
1✔
855
            for s, var in other_ns.interns.items():
1✔
856
                if not var.is_private:
1✔
857
                    final_refers = final_refers.assoc(s, var)
1✔
858
            self._refers = final_refers
1✔
859

860
    @classmethod
1✔
861
    def ns_cache(cls) -> lmap.PersistentMap:
1✔
862
        """Return a snapshot of the Namespace cache."""
863
        return cls._NAMESPACES.deref()
×
864

865
    @staticmethod
1✔
866
    def __get_or_create(
1✔
867
        ns_cache: NamespaceMap,
868
        name: sym.Symbol,
869
        module: BasilispModule | None = None,
870
    ) -> lmap.PersistentMap:
871
        """Private swap function used by `get_or_create` to atomically swap
872
        the new namespace map into the global cache."""
873
        ns = ns_cache.val_at(name, None)
1✔
874
        if ns is not None:
1✔
875
            return ns_cache
1✔
876
        new_ns = Namespace(name, module=module)
1✔
877

878
        # If this is a new namespace and we're given a module (typically from the
879
        # importer), attach the namespace to the module.
880
        if module is not None:
1✔
881
            logger.debug(f"Setting module '{module}' namespace to '{new_ns}'")
1✔
882
            module.__basilisp_namespace__ = new_ns
1✔
883

884
        # The `ns` macro is important for setting up a new namespace, but it becomes
885
        # available only after basilisp.core has been loaded.
886
        ns_var = Var.find_in_ns(CORE_NS_SYM, sym.symbol("ns"))
1✔
887
        if ns_var:
1✔
888
            new_ns.add_refer(sym.symbol("ns"), ns_var)
1✔
889

890
        return ns_cache.assoc(name, new_ns)
1✔
891

892
    @classmethod
1✔
893
    def get_or_create(
1✔
894
        cls, name: sym.Symbol, module: BasilispModule | None = None
895
    ) -> "Namespace":
896
        """Get the namespace bound to the symbol `name` in the global namespace
897
        cache, creating it if it does not exist.
898
        Return the namespace."""
899
        if (
1✔
900
            module is None
901
            and (module_var := Var.find(IMPORT_MODULE_VAR_SYM)) is not None
902
        ):
903
            module = module_var.value
1✔
904
            assert module is None or isinstance(module, BasilispModule)
1✔
905
        return cls._NAMESPACES.swap(Namespace.__get_or_create, name, module=module)[
1✔
906
            name
907
        ]
908

909
    @classmethod
1✔
910
    def get(cls, name: sym.Symbol) -> "Optional[Namespace]":
1✔
911
        """Get the namespace bound to the symbol `name` in the global namespace
912
        cache. Return the namespace if it exists or None otherwise.."""
913
        return cls._NAMESPACES.deref().val_at(name, None)
1✔
914

915
    @classmethod
1✔
916
    def remove(cls, name: sym.Symbol) -> Optional["Namespace"]:
1✔
917
        """Remove the namespace bound to the symbol `name` in the global
918
        namespace cache and return that namespace.
919
        Return None if the namespace did not exist in the cache."""
920
        if name == CORE_NS_SYM:
1✔
921
            raise ValueError("Cannot remove the Basilisp core namespace")
1✔
922
        while True:
1✔
923
            oldval: lmap.PersistentMap = cls._NAMESPACES.deref()
1✔
924
            ns: Namespace | None = oldval.val_at(name, None)
1✔
925
            newval = oldval
1✔
926
            if ns is not None:
1✔
927
                newval = oldval.dissoc(name)
1✔
928
            if cls._NAMESPACES.compare_and_set(oldval, newval):
1✔
929
                return ns
1✔
930

931
    # REPL Completion support
932

933
    @staticmethod
1✔
934
    def __completion_matcher(text: str) -> CompletionMatcher:
1✔
935
        """Return a function which matches any symbol keys from map entries
936
        against the given text."""
937

938
        def is_match(entry: tuple[sym.Symbol, Any]) -> bool:
1✔
939
            return entry[0].name.startswith(text)
1✔
940

941
        return is_match
1✔
942

943
    def __complete_alias(
1✔
944
        self, prefix: str, name_in_ns: str | None = None
945
    ) -> Iterable[str]:
946
        """Return an iterable of possible completions matching the given
947
        prefix from the list of aliased namespaces. If name_in_ns is given,
948
        further attempt to refine the list to matching names in that namespace."""
949
        candidates = filter(
1✔
950
            Namespace.__completion_matcher(prefix),
951
            ((s, n) for s, n in self.aliases.items()),
952
        )
953
        if name_in_ns is not None:
1✔
954
            for _, candidate_ns in candidates:
1✔
955
                for match in candidate_ns.__complete_interns(
1✔
956
                    name_in_ns, include_private_vars=False
957
                ):
958
                    yield f"{prefix}/{match}"
1✔
959
        else:
960
            for alias, _ in candidates:
1✔
961
                yield f"{alias}/"
1✔
962

963
    def __complete_imports_and_aliases(
1✔
964
        self, prefix: str, name_in_module: str | None = None
965
    ) -> Iterable[str]:
966
        """Return an iterable of possible completions matching the given
967
        prefix from the list of imports and aliased imports. If name_in_module
968
        is given, further attempt to refine the list to matching names in that
969
        namespace."""
970
        imports = self.imports
1✔
971
        aliases = lmap.map(
1✔
972
            {
973
                alias: imports.val_at(import_name)
974
                for alias, import_name in self.import_aliases.items()
975
            }
976
        )
977

978
        candidates = filter(
1✔
979
            Namespace.__completion_matcher(prefix),
980
            itertools.chain(
981
                aliases.items(), imports.items(), [(sym.symbol("python"), builtins)]
982
            ),
983
        )
984
        if name_in_module is not None:
1✔
985
            for _, module in candidates:
1✔
986
                for name in module.__dict__:
1✔
987
                    if name.startswith(name_in_module):
1✔
988
                        yield f"{prefix}/{name}"
1✔
989
        else:
990
            for candidate_name, _ in candidates:
1✔
991
                yield f"{candidate_name}/"
1✔
992

993
    def __complete_interns(
1✔
994
        self, value: str, include_private_vars: bool = True
995
    ) -> Iterable[str]:
996
        """Return an iterable of possible completions matching the given
997
        prefix from the list of interned Vars."""
998
        if include_private_vars:
1✔
999
            is_match = Namespace.__completion_matcher(value)
1✔
1000
        else:
1001
            _is_match = Namespace.__completion_matcher(value)
1✔
1002

1003
            def is_match(entry: tuple[sym.Symbol, Var]) -> bool:
1✔
1004
                return _is_match(entry) and not entry[1].is_private
1✔
1005

1006
        return map(
1✔
1007
            lambda entry: f"{entry[0].name}",
1008
            filter(is_match, ((s, v) for s, v in self.interns.items())),
1009
        )
1010

1011
    def __complete_refers(self, value: str) -> Iterable[str]:
1✔
1012
        """Return an iterable of possible completions matching the given prefix from
1013
        the list of referred Vars and referred Python module members."""
1014
        return map(
1✔
1015
            lambda entry: f"{entry[0].name}",
1016
            filter(
1017
                Namespace.__completion_matcher(value),
1018
                itertools.chain(
1019
                    ((s, v) for s, v in self.refers.items()),
1020
                    ((s, v) for s, v in self.import_refers.items()),
1021
                ),
1022
            ),
1023
        )
1024

1025
    def complete(self, text: str) -> Iterable[str]:
1✔
1026
        """Return an iterable of possible completions for the given text in
1027
        this namespace."""
1028
        assert not text.startswith(":")
1✔
1029

1030
        if "/" in text:
1✔
1031
            prefix, suffix = text.split("/", maxsplit=1)
1✔
1032
            results = itertools.chain(
1✔
1033
                self.__complete_alias(prefix, name_in_ns=suffix),
1034
                self.__complete_imports_and_aliases(prefix, name_in_module=suffix),
1035
            )
1036
        else:
1037
            results = itertools.chain(
1✔
1038
                self.__complete_alias(text),
1039
                self.__complete_imports_and_aliases(text),
1040
                self.__complete_interns(text),
1041
                self.__complete_refers(text),
1042
            )
1043

1044
        return results
1✔
1045

1046

1047
def get_thread_bindings() -> IPersistentMap[Var, Any]:
1✔
1048
    """Return the current thread-local bindings."""
1049
    bindings = {}
1✔
1050
    for frame in _THREAD_BINDINGS.get_bindings():
1✔
1051
        bindings.update({var: var.value for var in frame})
1✔
1052
    return lmap.map(bindings)
1✔
1053

1054

1055
def push_thread_bindings(m: IPersistentMap[Var, Any]) -> None:
1✔
1056
    """Push thread local bindings for the Var keys in m using the values."""
1057
    bindings = set()
1✔
1058

1059
    for var, val in m.items():
1✔
1060
        if not var.dynamic:
1✔
1061
            raise RuntimeException(
1✔
1062
                "cannot set thread-local bindings for non-dynamic Var"
1063
            )
1064
        var.push_bindings(val)
1✔
1065
        bindings.add(var)
1✔
1066

1067
    _THREAD_BINDINGS.push_bindings(lset.set(bindings))
1✔
1068

1069

1070
def pop_thread_bindings() -> None:
1✔
1071
    """Pop the thread local bindings set by push_thread_bindings above."""
1072
    try:
1✔
1073
        bindings = _THREAD_BINDINGS.pop_bindings()
1✔
1074
    except IndexError as e:
1✔
1075
        raise RuntimeException(
1✔
1076
            "cannot pop thread-local bindings without prior push"
1077
        ) from e
1078

1079
    for var in bindings:
1✔
1080
        var.pop_bindings()
1✔
1081

1082

1083
###################
1084
# Runtime Support #
1085
###################
1086

1087
T = TypeVar("T")
1✔
1088

1089

1090
@functools.singledispatch
1✔
1091
def to_set(s):
1✔
1092
    return lset.set(s)
1✔
1093

1094

1095
@to_set.register(type(None))
1✔
1096
def _to_set_none(_: None) -> lset.PersistentSet:
1✔
1097
    return lset.EMPTY
1✔
1098

1099

1100
@to_set.register(IPersistentMap)
1✔
1101
def _to_set_map(m: IPersistentMap) -> lset.PersistentSet:
1✔
1102
    if (s := m.seq()) is None:
1✔
1103
        return lset.EMPTY
1✔
1104
    return to_set(s)
1✔
1105

1106

1107
@functools.singledispatch
1✔
1108
def vector(v):
1✔
1109
    return vec.vector(v)
1✔
1110

1111

1112
@vector.register(type(None))
1✔
1113
def _vector_none(_: None) -> vec.PersistentVector:
1✔
1114
    return vec.EMPTY
1✔
1115

1116

1117
@vector.register(IPersistentMap)
1✔
1118
def _vector_map(m: IPersistentMap) -> vec.PersistentVector:
1✔
1119
    if (s := m.seq()) is None:
1✔
1120
        return vec.EMPTY
1✔
1121
    return vector(s)
1✔
1122

1123

1124
def keyword(name: Any, ns: Any = None) -> kw.Keyword:
1✔
1125
    """Return a new keyword with runtime type checks for name and namespace."""
1126
    if not isinstance(name, str):
1✔
1127
        raise TypeError(f"Keyword name must be a string, not '{type(name)}'")
1✔
1128
    if not isinstance(ns, (type(None), str)):
1✔
1129
        raise TypeError(f"Keyword namespace must be a string or nil, not '{type(ns)}")
×
1130
    return kw.keyword(name, ns)
1✔
1131

1132

1133
@functools.singledispatch
1✔
1134
def keyword_from_name(o: Any) -> NoReturn:
1✔
1135
    raise TypeError(f"Cannot create keyword from '{type(o)}'")
1✔
1136

1137

1138
@keyword_from_name.register(kw.Keyword)
1✔
1139
def _keyword_from_name_keyword(o: kw.Keyword) -> kw.Keyword:
1✔
1140
    return o
1✔
1141

1142

1143
@keyword_from_name.register(sym.Symbol)
1✔
1144
def _keyword_from_name_symbol(o: sym.Symbol) -> kw.Keyword:
1✔
1145
    return kw.keyword(o.name, ns=o.ns)
1✔
1146

1147

1148
@keyword_from_name.register(str)
1✔
1149
def _keyword_from_name_str(o: str) -> kw.Keyword:
1✔
1150
    if "/" in o and o != "/":
1✔
1151
        ns, name = o.split("/", maxsplit=1)
1✔
1152
        return kw.keyword(name, ns=ns)
1✔
1153
    return kw.keyword(o)
1✔
1154

1155

1156
def symbol(name: Any, ns: Any = None) -> sym.Symbol:
1✔
1157
    """Return a new symbol with runtime type checks for name and namespace."""
1158
    if not isinstance(name, str):
1✔
1159
        raise TypeError(f"Symbol name must be a string, not '{type(name)}'")
1✔
1160
    if not isinstance(ns, (type(None), str)):
1✔
1161
        raise TypeError(f"Symbol namespace must be a string or nil, not '{type(ns)}")
×
1162
    return sym.symbol(name, ns)
1✔
1163

1164

1165
@functools.singledispatch
1✔
1166
def symbol_from_name(o: Any) -> NoReturn:
1✔
1167
    raise TypeError(f"Cannot create symbol from '{type(o)}'")
1✔
1168

1169

1170
@symbol_from_name.register(kw.Keyword)
1✔
1171
def _symbol_from_name_keyword(o: kw.Keyword) -> sym.Symbol:
1✔
1172
    return sym.symbol(o.name, ns=o.ns)
1✔
1173

1174

1175
@symbol_from_name.register(sym.Symbol)
1✔
1176
def _symbol_from_name_symbol(o: sym.Symbol) -> sym.Symbol:
1✔
1177
    return o
1✔
1178

1179

1180
@symbol_from_name.register(str)
1✔
1181
def _symbol_from_name_str(o: str) -> sym.Symbol:
1✔
1182
    if "/" in o and o != "/":
1✔
1183
        ns, name = o.split("/", maxsplit=1)
1✔
1184
        return sym.symbol(name, ns=ns)
1✔
1185
    return sym.symbol(o)
1✔
1186

1187

1188
@symbol_from_name.register(Var)
1✔
1189
def _symbol_from_name_var(o: Var) -> sym.Symbol:
1✔
1190
    return sym.symbol(o.name.name, ns=o.ns.name)
1✔
1191

1192

1193
@functools.singledispatch
1✔
1194
def first(o):
1✔
1195
    """If o is a ISeq, return the first element from o. If o is None, return
1196
    None. Otherwise, coerces o to a Seq and returns the first."""
1197
    s = to_seq(o)
1✔
1198
    if s is None:
1✔
1199
        return None
1✔
1200
    return s.first
1✔
1201

1202

1203
@first.register(type(None))
1✔
1204
def _first_none(_: None) -> None:
1✔
1205
    return None
1✔
1206

1207

1208
@first.register(ISeq)
1✔
1209
def _first_iseq(o: ISeq[T]) -> T | None:
1✔
1210
    return o.first
1✔
1211

1212

1213
@functools.singledispatch
1✔
1214
def rest(o: Any) -> ISeq:
1✔
1215
    """If o is a ISeq, return the elements after the first in o. If o is None,
1216
    returns an empty seq. Otherwise, coerces o to a seq and returns the rest."""
1217
    n = to_seq(o)
1✔
1218
    if n is None:
1✔
1219
        return lseq.EMPTY
1✔
1220
    return n.rest
1✔
1221

1222

1223
@rest.register(type(None))
1✔
1224
def _rest_none(_: None) -> ISeq:
1✔
1225
    return lseq.EMPTY
1✔
1226

1227

1228
@rest.register(type(ISeq))
1✔
1229
def _rest_iseq(o: ISeq[T]) -> ISeq:
1✔
1230
    s = o.rest
×
1231
    if s is None:
×
1232
        return lseq.EMPTY
×
1233
    return s
×
1234

1235

1236
def nthrest(coll, i: int):
1✔
1237
    """Returns the nth rest sequence of coll, or coll if i is 0."""
1238
    while True:
1✔
1239
        if coll is None:
1✔
1240
            return None
1✔
1241
        if i == 0:
1✔
1242
            return coll
1✔
1243
        i -= 1
1✔
1244
        coll = rest(coll)
1✔
1245

1246

1247
def next_(o) -> ISeq | None:
1✔
1248
    """Calls rest on o. If o returns an empty sequence or None, returns None.
1249
    Otherwise, returns the elements after the first in o."""
1250
    return to_seq(rest(o))
1✔
1251

1252

1253
def nthnext(coll, i: int) -> ISeq | None:
1✔
1254
    """Returns the nth next sequence of coll."""
1255
    while True:
1✔
1256
        if coll is None:
1✔
1257
            return None
1✔
1258
        if i == 0:
1✔
1259
            return to_seq(coll)
1✔
1260
        i -= 1
1✔
1261
        coll = next_(coll)
1✔
1262

1263

1264
@functools.singledispatch
1✔
1265
def _cons(seq, o) -> ISeq:
1✔
1266
    return Maybe(to_seq(seq)).map(lambda s: s.cons(o)).or_else(lambda: llist.l(o))
1✔
1267

1268

1269
@_cons.register(type(None))
1✔
1270
def _cons_none(_: None, o: T) -> ISeq[T]:
1✔
1271
    return llist.l(o)
1✔
1272

1273

1274
@_cons.register(ISeq)
1✔
1275
def _cons_iseq(seq: ISeq, o) -> ISeq:
1✔
1276
    return seq.cons(o)
1✔
1277

1278

1279
def cons(o, seq) -> ISeq:
1✔
1280
    """Creates a new sequence where o is the first element and seq is the rest.
1281
    If seq is None, return a list containing o. If seq is not a ISeq, attempt
1282
    to coerce it to a ISeq and then cons o onto the resulting sequence."""
1283
    return _cons(seq, o)
1✔
1284

1285

1286
to_seq = lseq.to_seq
1✔
1287

1288
to_iterator_seq = lseq.iterator_sequence
1✔
1289

1290

1291
def is_reiterable_iterable(x: Any) -> bool:
1✔
1292
    """Return ``true`` if x is a re-iterable Iterable object."""
1293
    return isinstance(x, Iterable) and iter(x) is not x
1✔
1294

1295

1296
def concat(*seqs: Any) -> ISeq:
1✔
1297
    """Concatenate the sequences given by seqs into a single ISeq."""
1298
    return lseq.iterator_sequence(
1✔
1299
        itertools.chain.from_iterable(filter(None, map(to_seq, seqs)))
1300
    )
1301

1302

1303
T_reduce_init = TypeVar("T_reduce_init")
1✔
1304

1305

1306
@functools.singledispatch
1✔
1307
def internal_reduce(
1✔
1308
    coll: Any,
1309
    f: ReduceFunction,
1310
    init: T_reduce_init | object = IReduce.REDUCE_SENTINEL,
1311
) -> T_reduce_init:
1312
    raise TypeError(f"Type {type(coll)} cannot be reduced")
1✔
1313

1314

1315
@internal_reduce.register(collections.abc.Iterable)
1✔
1316
@internal_reduce.register(type(None))
1✔
1317
def _internal_reduce_iterable(
1✔
1318
    coll: Iterable[T] | None,
1319
    f: ReduceFunction[T_reduce_init, T],
1320
    init: T_reduce_init | object = IReduce.REDUCE_SENTINEL,
1321
) -> T_reduce_init:
1322
    s = to_seq(coll)
1✔
1323

1324
    if init is IReduce.REDUCE_SENTINEL:
1✔
1325
        if s is None:
1✔
1326
            return f()
1✔
1327

1328
        init, s = s.first, s.rest
1✔
1329
    else:
1330
        if s is None:
1✔
1331
            return cast(T_reduce_init, init)
1✔
1332

1333
    res = cast(T_reduce_init, init)
1✔
1334
    for item in s:
1✔
1335
        res = f(res, item)
1✔
1336
        if isinstance(res, Reduced):
1✔
1337
            return res.deref()
1✔
1338

1339
    return res
1✔
1340

1341

1342
@internal_reduce.register(IReduce)
1✔
1343
def _internal_reduce_ireduce(
1✔
1344
    coll: IReduce,
1345
    f: ReduceFunction[T_reduce_init, T],
1346
    init: T_reduce_init | object = IReduce.REDUCE_SENTINEL,
1347
) -> T_reduce_init:
1348
    if init is IReduce.REDUCE_SENTINEL:
1✔
1349
        return coll.reduce(f)
1✔
1350
    return coll.reduce(f, cast(T_reduce_init, init))
1✔
1351

1352

1353
def apply(f, args):
1✔
1354
    """Apply function f to the arguments provided.
1355
    The last argument must always be coercible to a Seq. Intermediate
1356
    arguments are not modified.
1357
    For example:
1358
        (apply max [1 2 3])   ;=> 3
1359
        (apply max 4 [1 2 3]) ;=> 4"""
1360
    final = list(args[:-1])
1✔
1361

1362
    try:
1✔
1363
        last = args[-1]
1✔
1364
    except TypeError as e:
×
1365
        logger.debug("Ignored %s: %s", type(e).__name__, e)
×
1366

1367
    s = to_seq(last)
1✔
1368
    if s is not None:
1✔
1369
        final.extend(s)
1✔
1370

1371
    return f(*final)
1✔
1372

1373

1374
def apply_kw(f, args):
1✔
1375
    """Apply function f to the arguments provided.
1376
    The last argument must always be coercible to a Mapping. Intermediate
1377
    arguments are not modified.
1378
    For example:
1379
        (apply python/dict {:a 1} {:b 2})   ;=> #py {:a 1 :b 2}
1380
        (apply python/dict {:a 1} {:a 2})   ;=> #py {:a 2}"""
1381
    final = list(args[:-1])
1✔
1382

1383
    try:
1✔
1384
        last = args[-1]
1✔
1385
    except TypeError as e:
×
1386
        logger.debug("Ignored %s: %s", type(e).__name__, e)
×
1387

1388
    if last is None:
1✔
1389
        kwargs = {}
1✔
1390
    else:
1391
        kwargs = {
1✔
1392
            to_py(k, lambda kw: munge(kw.name, allow_builtins=True)): v
1393
            for k, v in last.items()
1394
        }
1395

1396
    return f(*final, **kwargs)
1✔
1397

1398

1399
@functools.singledispatch
1✔
1400
def count(coll) -> int:
1✔
1401
    if isinstance(coll, Iterable) and iter(coll) is coll:
1✔
1402
        raise TypeError(
1✔
1403
            f"The count function is not supported on single-use iterable objects because it would exhaust them during counting. Please use iterator-seq to coerce them into sequences first. Iterable Object type: {type(coll)}"
1404
        )
1405

1406
    try:
1✔
1407
        return sum(1 for _ in coll)
1✔
1408
    except TypeError as e:
×
1409
        raise TypeError(f"count not supported on object of type {type(coll)}") from e
×
1410

1411

1412
@count.register(type(None))
1✔
1413
def _count_none(_: None) -> int:
1✔
1414
    return 0
1✔
1415

1416

1417
@count.register(Sized)
1✔
1418
def _count_sized(coll: Sized):
1✔
1419
    return len(coll)
1✔
1420

1421

1422
__nth_sentinel = object()
1✔
1423

1424

1425
@functools.singledispatch
1✔
1426
def nth(coll, i: int, notfound=__nth_sentinel):
1✔
1427
    """Returns the ith element of coll (0-indexed), if it exists.
1428
    None otherwise. If i is out of bounds, throws an IndexError unless
1429
    notfound is specified."""
1430
    raise TypeError(f"nth not supported on object of type {type(coll)}")
1✔
1431

1432

1433
@nth.register(type(None))
1✔
1434
def _nth_none(_: None, i: int, notfound=__nth_sentinel) -> None:
1✔
1435
    return notfound if notfound is not __nth_sentinel else None  # type: ignore[return-value]
1✔
1436

1437

1438
@nth.register(Sequence)
1✔
1439
def _nth_sequence(coll: Sequence, i: int, notfound=__nth_sentinel):
1✔
1440
    try:
1✔
1441
        return coll[i]
1✔
1442
    except IndexError as ex:
1✔
1443
        if notfound is not __nth_sentinel:
1✔
1444
            return notfound
1✔
1445
        raise ex
1✔
1446

1447

1448
@nth.register(ISeq)
1✔
1449
def _nth_iseq(coll: ISeq, i: int, notfound=__nth_sentinel):
1✔
1450
    for j, e in enumerate(coll):
1✔
1451
        if i == j:
1✔
1452
            return e
1✔
1453

1454
    if notfound is not __nth_sentinel:
1✔
1455
        return notfound
1✔
1456

1457
    raise IndexError(f"Index {i} out of bounds")
1✔
1458

1459

1460
@functools.singledispatch
1✔
1461
def contains(coll, k):
1✔
1462
    """Return true if o contains the key k."""
1463
    return k in coll
1✔
1464

1465

1466
@contains.register(type(None))
1✔
1467
def _contains_none(_, __):
1✔
1468
    return False
1✔
1469

1470

1471
@contains.register(IAssociative)
1✔
1472
def _contains_iassociative(coll: IAssociative, k):
1✔
1473
    return coll.contains(k)
1✔
1474

1475

1476
@contains.register(ITransientAssociative)
1✔
1477
def _contains_itransientassociative(coll: ITransientAssociative, k):
1✔
1478
    return coll.contains_transient(k)
1✔
1479

1480

1481
@functools.singledispatch
1✔
1482
def get(m, k, default=None):  # pylint: disable=unused-argument
1✔
1483
    """Return the value of k in m. Return default if k not found in m."""
1484
    return default
1✔
1485

1486

1487
@get.register(bytes)
1✔
1488
@get.register(dict)
1✔
1489
@get.register(list)
1✔
1490
@get.register(str)
1✔
1491
@get.register(bytes)
1✔
1492
def _get_others(m, k, default=None):
1✔
1493
    try:
1✔
1494
        return m[k]
1✔
1495
    except (KeyError, IndexError, TypeError):
1✔
1496
        return default
1✔
1497

1498

1499
@get.register(IPersistentSet)
1✔
1500
@get.register(ITransientSet)
1✔
1501
@get.register(frozenset)
1✔
1502
@get.register(set)
1✔
1503
def _get_settypes(m, k, default=None):
1✔
1504
    if k in m:
1✔
1505
        return k
1✔
1506
    return default
1✔
1507

1508

1509
@get.register(ILookup)
1✔
1510
def _get_ilookup(m, k, default=None):
1✔
1511
    return m.val_at(k, default)
1✔
1512

1513

1514
@functools.singledispatch
1✔
1515
def assoc(m, *kvs):
1✔
1516
    """Associate keys to values in associative data structure m. If m is None,
1517
    returns a new Map with key-values kvs."""
1518
    raise TypeError(
1✔
1519
        f"Object of type {type(m)} does not implement IAssociative interface"
1520
    )
1521

1522

1523
@assoc.register(type(None))
1✔
1524
def _assoc_none(_: None, *kvs) -> lmap.PersistentMap:
1✔
1525
    return lmap.EMPTY.assoc(*kvs)
1✔
1526

1527

1528
@assoc.register(IAssociative)
1✔
1529
def _assoc_iassociative(m: IAssociative, *kvs):
1✔
1530
    return m.assoc(*kvs)
1✔
1531

1532

1533
@functools.singledispatch
1✔
1534
def update(m, k, f, *args):
1✔
1535
    """Updates the value for key k in associative data structure m with the return value from
1536
    calling f(old_v, *args). If m is None, use an empty map. If k is not in m, old_v will be
1537
    None."""
1538
    raise TypeError(
1✔
1539
        f"Object of type {type(m)} does not implement IAssociative interface"
1540
    )
1541

1542

1543
@update.register(type(None))
1✔
1544
def _update_none(_: None, k, f, *args) -> lmap.PersistentMap:
1✔
1545
    return lmap.EMPTY.assoc(k, f(None, *args))
1✔
1546

1547

1548
@update.register(IAssociative)
1✔
1549
def _update_iassociative(m: IAssociative, k, f, *args):
1✔
1550
    old_v = m.val_at(k)
1✔
1551
    new_v = f(old_v, *args)
1✔
1552
    return m.assoc(k, new_v)
1✔
1553

1554

1555
@functools.singledispatch
1✔
1556
def keys(o):
1✔
1557
    raise TypeError(f"Object of type {type(o)} cannot be coerced to a key sequence")
1✔
1558

1559

1560
@keys.register(type(None))
1✔
1561
def _keys_none(_: None) -> None:
1✔
1562
    return None
1✔
1563

1564

1565
@keys.register(collections.abc.Iterable)
1✔
1566
@keys.register(ISeqable)
1✔
1567
def _keys_iterable(o: ISeqable | Iterable) -> ISeq | None:
1✔
1568
    return keys(to_seq(o))
1✔
1569

1570

1571
@keys.register(ISeq)
1✔
1572
def _keys_iseq(o: ISeq) -> ISeq | None:
1✔
1573
    def _key_seq(s: ISeq) -> ISeq | None:
1✔
1574
        if to_seq(s) is not None:
1✔
1575
            e = s.first
1✔
1576
            if not isinstance(e, IMapEntry):
1✔
1577
                raise TypeError(
1✔
1578
                    f"Object of type {type(e)} cannot be coerced to a map entry"
1579
                )
1580
            return lseq.Cons(e.key, lseq.LazySeq(lambda: _key_seq(s.rest)))
1✔
1581
        return None
1✔
1582

1583
    return lseq.LazySeq(lambda: _key_seq(o))
1✔
1584

1585

1586
@keys.register(collections.abc.Mapping)
1✔
1587
def _keys_mapping(o: Mapping) -> ISeq | None:
1✔
1588
    return to_seq(o.keys())
1✔
1589

1590

1591
@functools.singledispatch
1✔
1592
def vals(o):
1✔
1593
    raise TypeError(f"Object of type {type(o)} cannot be coerced to a value sequence")
1✔
1594

1595

1596
@vals.register(type(None))
1✔
1597
def _vals_none(_: None) -> None:
1✔
1598
    return None
1✔
1599

1600

1601
@keys.register(collections.abc.Iterable)
1✔
1602
@vals.register(ISeqable)
1✔
1603
def _vals_iterable(o: ISeqable | Iterable) -> ISeq | None:
1✔
1604
    return vals(to_seq(o))
1✔
1605

1606

1607
@vals.register(ISeq)
1✔
1608
def _vals_iseq(o: ISeq) -> ISeq | None:
1✔
1609
    def _val_seq(s: ISeq) -> ISeq | None:
1✔
1610
        if to_seq(s) is not None:
1✔
1611
            e = s.first
1✔
1612
            if not isinstance(e, IMapEntry):
1✔
1613
                raise TypeError(
1✔
1614
                    f"Object of type {type(e)} cannot be coerced to a map entry"
1615
                )
1616
            return lseq.Cons(e.value, lseq.LazySeq(lambda: _val_seq(s.rest)))
1✔
1617
        return None
1✔
1618

1619
    return lseq.LazySeq(lambda: _val_seq(o))
1✔
1620

1621

1622
@vals.register(collections.abc.Mapping)
1✔
1623
def _vals_mapping(o: Mapping) -> ISeq | None:
1✔
1624
    return to_seq(o.values())
1✔
1625

1626

1627
@functools.singledispatch
1✔
1628
def conj(coll, *xs):
1✔
1629
    """Conjoin xs to collection. New elements may be added in different positions
1630
    depending on the type of coll. conj returns the same type as coll. If coll
1631
    is None, return a list with xs conjoined."""
1632
    raise TypeError(
1✔
1633
        f"Object of type {type(coll)} does not implement "
1634
        "IPersistentCollection interface"
1635
    )
1636

1637

1638
@conj.register(type(None))
1✔
1639
def _conj_none(_: None, *xs):
1✔
1640
    l = llist.EMPTY
1✔
1641
    return l.cons(*xs)
1✔
1642

1643

1644
@conj.register(IPersistentCollection)
1✔
1645
def _conj_ipersistentcollection(coll: IPersistentCollection, *xs):
1✔
1646
    return coll.cons(*xs)
1✔
1647

1648

1649
def _update_signature_for_partial(f: BasilispFunction, num_args: int) -> None:
1✔
1650
    """Update the various properties of a Basilisp function for wrapped partials.
1651

1652
    Partial applications change the number of arities a function appears to have.
1653
    This function computes the new `arities` set for the partial function by removing
1654
    any now-invalid fixed arities from the original function's set.
1655

1656
    Additionally, partials do not take the meta from the wrapped function, so that
1657
    value should be cleared and the `with-meta` method should be replaced with a
1658
    new method."""
1659
    existing_arities: IPersistentSet[kw.Keyword | int] = f.arities
1✔
1660
    new_arities: set[kw.Keyword | int] = set()
1✔
1661
    for arity in existing_arities:
1✔
1662
        if isinstance(arity, kw.Keyword):
1✔
1663
            new_arities.add(arity)
1✔
1664
        elif arity > num_args:
1✔
1665
            new_arities.add(arity - num_args)
1✔
1666
    if not new_arities:
1✔
1667
        if num_args in existing_arities:
1✔
1668
            new_arities.add(0)
1✔
1669
        else:
1670
            logger.warning(
1✔
1671
                f"invalid partial function application of '{f.__name__}' detected: "  # type: ignore[attr-defined]
1672
                f"{num_args} arguments given; expected any of: "
1673
                f"{', '.join(sorted(map(str, existing_arities)))}"
1674
            )
1675
    f.arities = lset.set(new_arities)
1✔
1676
    f.meta = None
1✔
1677
    f.with_meta = partial(_fn_with_meta, f)  # type: ignore[method-assign]
1✔
1678

1679

1680
def partial(f, *args, **kwargs):
1✔
1681
    """Return a function which is the partial application of f with args and kwargs."""
1682

1683
    @functools.wraps(f)
1✔
1684
    def partial_f(*inner_args, **inner_kwargs):
1✔
1685
        return f(*args, *inner_args, **{**kwargs, **inner_kwargs})
1✔
1686

1687
    if hasattr(partial_f, "_basilisp_fn"):
1✔
1688
        _update_signature_for_partial(cast(BasilispFunction, partial_f), len(args))
1✔
1689

1690
    return partial_f
1✔
1691

1692

1693
@functools.singledispatch
1✔
1694
def deref(o, timeout_ms=None, timeout_val=None):
1✔
1695
    """Dereference a Deref object and return its contents.
1696

1697
    If o is an object implementing IBlockingDeref and timeout_ms and
1698
    timeout_val are supplied, deref will wait at most timeout_ms milliseconds,
1699
    returning timeout_val if timeout_ms milliseconds elapse and o has not
1700
    returned."""
1701
    raise TypeError(f"Object of type {type(o)} cannot be dereferenced")
1✔
1702

1703

1704
@deref.register(IBlockingDeref)
1✔
1705
def _deref_blocking(o: IBlockingDeref, timeout_ms: int | None = None, timeout_val=None):
1✔
1706
    timeout_s = None
1✔
1707
    if timeout_ms is not None:
1✔
1708
        timeout_s = timeout_ms / 1000 if timeout_ms != 0 else 0
1✔
1709
    return o.deref(timeout_s, timeout_val)
1✔
1710

1711

1712
@deref.register(IDeref)
1✔
1713
def _deref(o: IDeref):
1✔
1714
    return o.deref()
1✔
1715

1716

1717
def equals(v1, v2) -> bool:
1✔
1718
    """Compare two objects by value. Unlike the standard Python equality operator,
1719
    this function does not consider 1 == True or 0 == False. All other equality
1720
    operations are the same and performed using Python's equality operator."""
1721
    if isinstance(v1, (bool, type(None))) or isinstance(v2, (bool, type(None))):
1✔
1722
        return v1 is v2
1✔
1723
    return v1 == v2
1✔
1724

1725

1726
@functools.singledispatch
1✔
1727
def divide(x: LispNumber, y: LispNumber) -> LispNumber:
1✔
1728
    """Division reducer. If both arguments are integers, return a Fraction.
1729
    Otherwise, return the true division of x and y."""
1730
    return x / y
1✔
1731

1732

1733
@divide.register(int)
1✔
1734
def _divide_ints(x: int, y: LispNumber) -> LispNumber:
1✔
1735
    if isinstance(y, int):
1✔
1736
        frac = Fraction(x, y)
1✔
1737
        # fractions.Fraction.is_integer() wasn't added until 3.12
1738
        return frac.numerator if frac.denominator == 1 else frac
1✔
1739
    return x / y
1✔
1740

1741

1742
@functools.singledispatch
1✔
1743
def compare(x, y) -> int:
1✔
1744
    """Return either -1, 0, or 1 to indicate the relationship between x and y.
1745

1746
    This is a 3-way comparator commonly used in Java-derived systems. Python does not
1747
    typically use 3-way comparators, so this function convert's Python's `__lt__` and
1748
    `__gt__` method returns into one of the 3-way comparator return values."""
1749
    if y is None:
1✔
1750
        assert x is not None, "x cannot be nil"
1✔
1751
        return 1
1✔
1752
    return (x > y) - (x < y)
1✔
1753

1754

1755
@compare.register(type(None))
1✔
1756
def _compare_nil(_: None, y) -> int:
1✔
1757
    # nil is less than all values, except itself.
1758
    return 0 if y is None else -1
1✔
1759

1760

1761
@compare.register(decimal.Decimal)
1✔
1762
def _compare_decimal(x: decimal.Decimal, y) -> int:
1✔
1763
    # Decimal instances will not compare with float("nan"), so we need a special case
1764
    if isinstance(y, float):
1✔
1765
        return -compare(y, x)  # pylint: disable=arguments-out-of-order
1✔
1766
    return (x > y) - (x < y)
1✔
1767

1768

1769
@compare.register(float)
1✔
1770
def _compare_float(x, y) -> int:
1✔
1771
    if y is None:
1✔
1772
        return 1
1✔
1773
    if math.isnan(x):
1✔
1774
        return 0
1✔
1775
    return (x > y) - (x < y)
1✔
1776

1777

1778
@compare.register(IPersistentSet)
1✔
1779
def _compare_sets(x: IPersistentSet, y) -> int:
1✔
1780
    # Sets are not comparable (because there is no total ordering between sets).
1781
    # However, in Python comparison is done using __lt__ and __gt__, which AbstractSet
1782
    # inconveniently also uses as part of it's API for comparing sets with subset and
1783
    # superset relationships. To "break" that, we just override the comparison method.
1784
    # One consequence of this is that it may be possible to sort a collection of sets,
1785
    # since `compare` isn't actually consulted in sorting.
1786
    raise TypeError(
1✔
1787
        f"cannot compare instances of '{type(x).__name__}' and '{type(y).__name__}'"
1788
    )
1789

1790

1791
def _fn_to_comparator(f):
1✔
1792
    """Coerce F comparator fn to a 3 way comparator fn."""
1793

1794
    if f == compare:  # pylint: disable=comparison-with-callable
1✔
1795
        return f
1✔
1796

1797
    def cmp(x, y):
1✔
1798
        r = f(x, y)
1✔
1799
        if not isinstance(r, bool) and isinstance(r, numbers.Number):
1✔
1800
            return r
1✔
1801
        elif r:
1✔
1802
            return -1
1✔
1803
        elif f(y, x):
1✔
1804
            return 1
1✔
1805
        else:
1806
            return 0
1✔
1807

1808
    return cmp
1✔
1809

1810

1811
def sort(coll, f=compare) -> ISeq | None:
1✔
1812
    """Return a sorted sequence of the elements in coll. If a
1813
    comparator function f is provided, compare elements in coll
1814
    using f or use the `compare` fn if not.
1815

1816
    The comparator fn can be either a boolean or 3-way comparison fn."""
1817
    seq = lseq.to_seq(coll)
1✔
1818
    if seq:
1✔
1819
        if isinstance(coll, IPersistentMap):
1✔
1820
            coll = seq
1✔
1821

1822
        comparator = _fn_to_comparator(f)
1✔
1823

1824
        class key:
1✔
1825
            __slots__ = ("obj",)
1✔
1826

1827
            def __init__(self, obj):
1✔
1828
                self.obj = obj
1✔
1829

1830
            def __lt__(self, other):
1✔
1831
                return comparator(self.obj, other.obj) < 0
1✔
1832

1833
            def __gt__(self, other):
1✔
UNCOV
1834
                return comparator(self.obj, other.obj) > 0
×
1835

1836
            def __eq__(self, other):
1✔
1837
                return comparator(self.obj, other.obj) == 0
×
1838

1839
            def __le__(self, other):
1✔
1840
                return comparator(self.obj, other.obj) <= 0
×
1841

1842
            def __ge__(self, other):
1✔
1843
                return comparator(self.obj, other.obj) >= 0
×
1844

1845
            __hash__ = None  # type: ignore
1✔
1846

1847
        return lseq.sequence(sorted(coll, key=key))
1✔
1848
    else:
1849
        return llist.EMPTY
1✔
1850

1851

1852
def sort_by(keyfn, coll, cmp=compare) -> ISeq | None:
1✔
1853
    """Return a sorted sequence of the elements in coll. If a
1854
    comparator function cmp is provided, compare elements in coll
1855
    using cmp or use the `compare` fn if not.
1856

1857
    The comparator fn can be either a boolean or 3-way comparison fn."""
1858
    seq = lseq.to_seq(coll)
1✔
1859
    if seq:
1✔
1860
        if isinstance(coll, IPersistentMap):
1✔
1861
            coll = seq
1✔
1862

1863
        comparator = _fn_to_comparator(cmp)
1✔
1864

1865
        class key:
1✔
1866
            __slots__ = ("obj",)
1✔
1867

1868
            def __init__(self, obj):
1✔
1869
                self.obj = obj
1✔
1870

1871
            def __lt__(self, other):
1✔
1872
                return comparator(keyfn(self.obj), keyfn(other.obj)) < 0
1✔
1873

1874
            def __gt__(self, other):
1✔
UNCOV
1875
                return comparator(keyfn(self.obj), keyfn(other.obj)) > 0
×
1876

1877
            def __eq__(self, other):
1✔
1878
                return comparator(keyfn(self.obj), keyfn(other.obj)) == 0
×
1879

1880
            def __le__(self, other):
1✔
1881
                return comparator(keyfn(self.obj), keyfn(other.obj)) <= 0
×
1882

1883
            def __ge__(self, other):
1✔
1884
                return comparator(keyfn(self.obj), keyfn(other.obj)) >= 0
×
1885

1886
            __hash__ = None  # type: ignore
1✔
1887

1888
        return lseq.sequence(sorted(coll, key=key))
1✔
1889
    else:
1890
        return llist.EMPTY
1✔
1891

1892

1893
def is_special_form(s: sym.Symbol) -> bool:
1✔
1894
    """Return True if s names a special form."""
1895
    return s in _SPECIAL_FORMS
1✔
1896

1897

1898
@functools.singledispatch
1✔
1899
def to_lisp(o, keywordize_keys: bool = True):  # pylint: disable=unused-argument
1✔
1900
    """Recursively convert Python collections into Lisp collections."""
1901
    return o
1✔
1902

1903

1904
@to_lisp.register(list)
1✔
1905
@to_lisp.register(tuple)
1✔
1906
def _to_lisp_vec(o: Iterable, keywordize_keys: bool = True) -> vec.PersistentVector:
1✔
1907
    return vec.vector(
1✔
1908
        map(functools.partial(to_lisp, keywordize_keys=keywordize_keys), o)
1909
    )
1910

1911

1912
@functools.singledispatch
1✔
1913
def _keywordize_keys(k, keywordize_keys: bool = True):
1✔
1914
    return to_lisp(k, keywordize_keys=keywordize_keys)
1✔
1915

1916

1917
@_keywordize_keys.register(str)
1✔
1918
def _keywordize_keys_str(k: str, keywordize_keys: bool = True):
1✔
1919
    return keyword_from_name(k)
1✔
1920

1921

1922
@to_lisp.register(dict)
1✔
1923
def _to_lisp_map(o: Mapping, keywordize_keys: bool = True) -> lmap.PersistentMap:
1✔
1924
    process_key = _keywordize_keys if keywordize_keys else to_lisp
1✔
1925
    return lmap.map(
1✔
1926
        {
1927
            process_key(k, keywordize_keys=keywordize_keys): to_lisp(
1928
                v, keywordize_keys=keywordize_keys
1929
            )
1930
            for k, v in o.items()
1931
        }
1932
    )
1933

1934

1935
@to_lisp.register(frozenset)
1✔
1936
@to_lisp.register(set)
1✔
1937
def _to_lisp_set(o: AbstractSet, keywordize_keys: bool = True) -> lset.PersistentSet:
1✔
1938
    return lset.set(map(functools.partial(to_lisp, keywordize_keys=keywordize_keys), o))
1✔
1939

1940

1941
def _kw_name(kw: kw.Keyword) -> str:
1✔
1942
    return kw.name
1✔
1943

1944

1945
@functools.singledispatch
1✔
1946
def to_py(
1✔
1947
    o, keyword_fn: Callable[[kw.Keyword], Any] = _kw_name
1948
):  # pylint: disable=unused-argument
1949
    """Recursively convert Lisp collections into Python collections."""
1950
    return o
1✔
1951

1952

1953
@to_py.register(kw.Keyword)
1✔
1954
def _to_py_kw(o: kw.Keyword, keyword_fn: Callable[[kw.Keyword], Any] = _kw_name) -> Any:
1✔
1955
    return keyword_fn(o)
1✔
1956

1957

1958
@to_py.register(IPersistentList)
1✔
1959
@to_py.register(ISeq)
1✔
1960
@to_py.register(IPersistentVector)
1✔
1961
def _to_py_list(
1✔
1962
    o: IPersistentList | ISeq | IPersistentVector,
1963
    keyword_fn: Callable[[kw.Keyword], Any] = _kw_name,
1964
) -> list:
1965
    return list(map(functools.partial(to_py, keyword_fn=keyword_fn), o))
1✔
1966

1967

1968
@to_py.register(IPersistentMap)
1✔
1969
def _to_py_map(
1✔
1970
    o: IPersistentMap, keyword_fn: Callable[[kw.Keyword], Any] = _kw_name
1971
) -> dict:
1972
    return {
1✔
1973
        to_py(key, keyword_fn=keyword_fn): to_py(value, keyword_fn=keyword_fn)
1974
        for key, value in o.items()
1975
    }
1976

1977

1978
@to_py.register(IPersistentSet)
1✔
1979
def _to_py_set(
1✔
1980
    o: IPersistentSet, keyword_fn: Callable[[kw.Keyword], Any] = _kw_name
1981
) -> set:
1982
    return {to_py(e, keyword_fn=keyword_fn) for e in o}
1✔
1983

1984

1985
def lrepr(o, human_readable: bool = False) -> str:
1✔
1986
    """Produce a string representation of an object. If human_readable is False,
1987
    the string representation of Lisp objects is something that can be read back
1988
    in by the reader as the same object."""
1989
    core_ns = Namespace.get(CORE_NS_SYM)
1✔
1990
    assert core_ns is not None
1✔
1991
    return lobj.lrepr(
1✔
1992
        o,
1993
        human_readable=human_readable,
1994
        print_dup=core_ns.find(sym.symbol(PRINT_DUP_VAR_NAME)).value,  # type: ignore
1995
        print_length=core_ns.find(  # type: ignore
1996
            sym.symbol(PRINT_LENGTH_VAR_NAME)
1997
        ).value,
1998
        print_level=core_ns.find(  # type: ignore
1999
            sym.symbol(PRINT_LEVEL_VAR_NAME)
2000
        ).value,
2001
        print_meta=core_ns.find(sym.symbol(PRINT_META_VAR_NAME)).value,  # type: ignore
2002
        print_namespace_maps=core_ns.find(sym.symbol(PRINT_NAMESPACE_MAPS_VAR_NAME)).value,  # type: ignore
2003
        print_readably=core_ns.find(  # type: ignore
2004
            sym.symbol(PRINT_READABLY_VAR_NAME)
2005
        ).value,
2006
    )
2007

2008

2009
def lstr(o) -> str:
1✔
2010
    """Produce a human-readable string representation of an object."""
2011
    return lobj.lstr(o)
1✔
2012

2013

2014
__NOT_COMPLETEABLE = re.compile(r"^[0-9].*")
1✔
2015

2016

2017
def repl_completions(text: str) -> Iterable[str]:
1✔
2018
    """Return an optional iterable of REPL completions."""
2019
    # Can't complete Keywords, Numerals
2020
    if __NOT_COMPLETEABLE.match(text):
1✔
UNCOV
2021
        return ()
×
2022
    elif text.startswith(":"):
1✔
UNCOV
2023
        return kw.complete(text)
×
2024
    else:
2025
        ns = get_current_ns()
1✔
2026
        return ns.complete(text)
1✔
2027

2028

2029
####################
2030
# Compiler Support #
2031
####################
2032

2033

2034
class _TrampolineArgs:
1✔
2035
    __slots__ = ("_has_varargs", "_args", "_kwargs")
1✔
2036

2037
    def __init__(self, has_varargs: bool, *args, **kwargs) -> None:
1✔
2038
        self._has_varargs = has_varargs
1✔
2039
        self._args = args
1✔
2040
        self._kwargs = kwargs
1✔
2041

2042
    @property
1✔
2043
    def args(self) -> tuple:
1✔
2044
        """Return the arguments for a trampolined function. If the function
2045
        that is being trampolined has varargs, unroll the final argument if
2046
        it is a sequence."""
2047
        if not self._has_varargs:
1✔
2048
            return self._args
1✔
2049

2050
        try:
1✔
2051
            final = self._args[-1]
1✔
2052
            if isinstance(final, ISeq):
1✔
2053
                inits = self._args[:-1]
1✔
2054
                return tuple(itertools.chain(inits, final))
1✔
2055
            return self._args
1✔
2056
        except IndexError:
1✔
2057
            return ()
1✔
2058

2059
    @property
1✔
2060
    def kwargs(self) -> dict:
1✔
2061
        return self._kwargs
1✔
2062

2063

2064
def _trampoline(f):
1✔
2065
    """Trampoline a function repeatedly until it is finished recurring to help
2066
    avoid stack growth."""
2067

2068
    @functools.wraps(f)
1✔
2069
    def trampoline(*args, **kwargs):
1✔
2070
        while True:
1✔
2071
            ret = f(*args, **kwargs)
1✔
2072
            if isinstance(ret, _TrampolineArgs):
1✔
2073
                args = ret.args
1✔
2074
                kwargs = ret.kwargs
1✔
2075
                continue
1✔
2076
            return ret
1✔
2077

2078
    return trampoline
1✔
2079

2080

2081
def _lisp_fn_apply_kwargs(f):
1✔
2082
    """Convert a Python function into a Lisp function.
2083

2084
    Python keyword arguments will be converted into Lisp keyword/argument pairs
2085
    that can be easily understood by Basilisp.
2086

2087
    Lisp functions annotated with the `:apply` value for the `:kwargs` metadata key
2088
    will be wrapped with this decorator by the compiler."""
2089

2090
    @functools.wraps(f)
1✔
2091
    def wrapped_f(*args, **kwargs):
1✔
2092
        return f(
1✔
2093
            *args,
2094
            *itertools.chain.from_iterable(
2095
                (kw.keyword(demunge(k)), v) for k, v in kwargs.items()
2096
            ),
2097
        )
2098

2099
    return wrapped_f
1✔
2100

2101

2102
def _lisp_fn_collect_kwargs(f):
1✔
2103
    """Convert a Python function into a Lisp function.
2104

2105
    Python keyword arguments will be collected into a single map, which is supplied
2106
    as the final positional argument.
2107

2108
    Lisp functions annotated with the `:collect` value for the `:kwargs` metadata key
2109
    will be wrapped with this decorator by the compiler."""
2110

2111
    @functools.wraps(f)
1✔
2112
    def wrapped_f(*args, **kwargs):
1✔
2113
        return f(
1✔
2114
            *args,
2115
            lmap.map({kw.keyword(demunge(k)): v for k, v in kwargs.items()}),
2116
        )
2117

2118
    return wrapped_f
1✔
2119

2120

2121
def _with_attrs(**kwargs):
1✔
2122
    """Decorator to set attributes on a function. Returns the original
2123
    function after setting the attributes named by the keyword arguments."""
2124

2125
    def decorator(f):
1✔
2126
        for k, v in kwargs.items():
1✔
2127
            setattr(f, k, v)
1✔
2128
        return f
1✔
2129

2130
    return decorator
1✔
2131

2132

2133
def _fn_with_meta(f, meta: lmap.PersistentMap | None):
1✔
2134
    """Return a new function with the given meta. If the function f already
2135
    has a meta map, then merge the new meta with the existing meta."""
2136

2137
    if not isinstance(meta, lmap.PersistentMap):
1✔
2138
        raise TypeError("meta must be a map")
1✔
2139

2140
    if inspect.iscoroutinefunction(f):
1✔
2141

2142
        @functools.wraps(f)
1✔
2143
        async def wrapped_f(*args, **kwargs):
1✔
2144
            return await f(*args, **kwargs)
1✔
2145

2146
    else:
2147

2148
        @functools.wraps(f)
1✔
2149
        def wrapped_f(*args, **kwargs):
1✔
2150
            return f(*args, **kwargs)
1✔
2151

2152
    wrapped_f.meta = (  # type: ignore
1✔
2153
        f.meta.update(meta)
2154
        if hasattr(f, "meta") and isinstance(f.meta, lmap.PersistentMap)
2155
        else meta
2156
    )
2157
    wrapped_f.with_meta = partial(_fn_with_meta, wrapped_f)  # type: ignore
1✔
2158
    return wrapped_f
1✔
2159

2160

2161
def _basilisp_fn(
1✔
2162
    arities: tuple[int | kw.Keyword, ...],
2163
) -> Callable[..., BasilispFunction]:
2164
    """Create a Basilisp function, setting meta and supplying a with_meta
2165
    method implementation."""
2166

2167
    def wrap_fn(f) -> BasilispFunction:
1✔
2168
        assert not hasattr(f, "meta")
1✔
2169
        f._basilisp_fn = True
1✔
2170
        f.arities = lset.set(arities)
1✔
2171
        f.meta = None
1✔
2172
        f.with_meta = partial(_fn_with_meta, f)
1✔
2173
        return f
1✔
2174

2175
    return wrap_fn
1✔
2176

2177

2178
def _basilisp_type(
1✔
2179
    fields: Iterable[str],
2180
    interfaces: Iterable[type],
2181
    artificially_abstract_bases: AbstractSet[type],
2182
    members: Iterable[str],
2183
):
2184
    """Check that a Basilisp type (defined by `deftype*`) only declares abstract
2185
    super-types and that all abstract methods are implemented."""
2186

2187
    def wrap_class(cls: type):
1✔
2188
        field_names = frozenset(fields)
1✔
2189
        member_names = frozenset(members)
1✔
2190
        artificially_abstract_base_members: set[str] = set()
1✔
2191
        all_member_names = field_names.union(member_names)
1✔
2192
        all_interface_methods: set[str] = set()
1✔
2193
        for interface in interfaces:
1✔
2194
            if interface is object:
1✔
UNCOV
2195
                continue
×
2196

2197
            if is_abstract(interface):
1✔
2198
                interface_names: frozenset[str] = interface.__abstractmethods__  # type: ignore[attr-defined]
1✔
2199
                interface_property_names: frozenset[str] = frozenset(
1✔
2200
                    method
2201
                    for method in interface_names
2202
                    if isinstance(getattr(interface, method), property)
2203
                )
2204
                interface_method_names = interface_names - interface_property_names
1✔
2205
                if not interface_method_names.issubset(member_names):
1✔
2206
                    missing_methods = ", ".join(interface_method_names - member_names)
1✔
2207
                    raise RuntimeException(
1✔
2208
                        "deftype* definition missing interface members for interface "
2209
                        f"{interface}: {missing_methods}",
2210
                    )
2211
                elif not interface_property_names.issubset(all_member_names):
1✔
2212
                    missing_fields = ", ".join(interface_property_names - field_names)
1✔
2213
                    raise RuntimeException(
1✔
2214
                        "deftype* definition missing interface properties for interface "
2215
                        f"{interface}: {missing_fields}",
2216
                    )
2217

2218
                all_interface_methods.update(interface_names)
1✔
2219
            elif interface in artificially_abstract_bases:
1✔
2220
                artificially_abstract_base_members.update(
1✔
2221
                    map(
2222
                        lambda v: v[0],
2223
                        inspect.getmembers(
2224
                            interface,
2225
                            predicate=lambda v: inspect.isfunction(v)
2226
                            or isinstance(v, (property, staticmethod))
2227
                            or inspect.ismethod(v),
2228
                        ),
2229
                    )
2230
                )
2231
            else:
2232
                raise RuntimeException(
1✔
2233
                    "deftype* interface must be Python abstract class or object",
2234
                )
2235

2236
        extra_methods = member_names - all_interface_methods - OBJECT_DUNDER_METHODS
1✔
2237
        if extra_methods and not extra_methods.issubset(
1✔
2238
            artificially_abstract_base_members
2239
        ):
2240
            extra_method_str = ", ".join(extra_methods)
1✔
2241
            raise RuntimeException(
1✔
2242
                "deftype* definition for interface includes members not part of "
2243
                f"defined interfaces: {extra_method_str}"
2244
            )
2245

UNCOV
2246
        return cls
×
2247

2248
    return wrap_class
1✔
2249

2250

2251
def _load_constant(s: bytes) -> Any:
1✔
2252
    """Load a compiler "constant" stored as a byte string as by Python's `pickle`
2253
    module.
2254

2255
    Constant types without special handling are emitted to bytecode as a byte string
2256
    produced by `pickle.dumps`."""
2257
    try:
1✔
2258
        return pickle.loads(s)  # nosec B301
1✔
UNCOV
2259
    except pickle.UnpicklingError as e:
×
UNCOV
2260
        raise RuntimeException("Unable to load constant value") from e
×
2261

2262

2263
###############################
2264
# Symbol and Alias Resolution #
2265
###############################
2266

2267

2268
def resolve_alias(s: sym.Symbol, ns: Namespace | None = None) -> sym.Symbol:
1✔
2269
    """Resolve the aliased symbol in the current namespace."""
2270
    if s in _SPECIAL_FORMS:
1✔
2271
        return s
1✔
2272

2273
    ns = Maybe(ns).or_else(get_current_ns)
1✔
2274
    if s.ns is not None:
1✔
2275
        aliased_ns = ns.get_alias(sym.symbol(s.ns))
1✔
2276
        if aliased_ns is not None:
1✔
2277
            return sym.symbol(s.name, aliased_ns.name)
1✔
2278
        else:
2279
            return s
1✔
2280
    else:
2281
        which_var = ns.find(sym.symbol(s.name))
1✔
2282
        if which_var is not None:
1✔
2283
            return sym.symbol(which_var.name.name, which_var.ns.name)
1✔
2284
        else:
2285
            return sym.symbol(s.name, ns=ns.name)
1✔
2286

2287

2288
def resolve_var(s: sym.Symbol, ns: Namespace | None = None) -> Var | None:
1✔
2289
    """Resolve the aliased symbol to a Var from the specified namespace, or the
2290
    current namespace if none is specified."""
2291
    ns_qualified_sym = resolve_alias(s, ns)
1✔
2292
    return Var.find(resolve_alias(s, ns)) if ns_qualified_sym.ns else None
1✔
2293

2294

2295
#######################
2296
# Namespace Utilities #
2297
#######################
2298

2299

2300
@contextlib.contextmanager
1✔
2301
def bindings(bindings: Mapping[Var, Any] | None = None):
1✔
2302
    """Context manager for temporarily changing the value thread-local value for
2303
    Basilisp dynamic Vars."""
2304
    m = lmap.map(bindings or {})
1✔
2305
    logger.debug(
1✔
2306
        f"Binding thread-local values for Vars: {', '.join(map(str, m.keys()))}"
2307
    )
2308
    try:
1✔
2309
        push_thread_bindings(m)
1✔
2310
        yield
1✔
2311
    finally:
2312
        pop_thread_bindings()
1✔
2313
        logger.debug(
1✔
2314
            f"Reset thread-local bindings for Vars: {', '.join(map(str, m.keys()))}"
2315
        )
2316

2317

2318
@contextlib.contextmanager
1✔
2319
def ns_bindings(
1✔
2320
    ns_name: str, module: BasilispModule | None = None
2321
) -> Iterator[Namespace]:
2322
    """Context manager for temporarily changing the value of basilisp.core/*ns*."""
2323
    symbol = sym.symbol(ns_name)
1✔
2324
    ns = Namespace.get_or_create(symbol, module=module)
1✔
2325
    ns_var = Maybe(Var.find(NS_VAR_SYM)).or_else_raise(
1✔
2326
        lambda: RuntimeException(f"Dynamic Var {NS_VAR_SYM} not bound!")
2327
    )
2328

2329
    with bindings({ns_var: ns}):
1✔
2330
        yield ns_var.value
1✔
2331

2332

2333
@contextlib.contextmanager
1✔
2334
def remove_ns_bindings():
1✔
2335
    """Context manager to pop the most recent bindings for basilisp.core/*ns* after
2336
    completion of the code under management."""
2337
    ns_var = Maybe(Var.find(NS_VAR_SYM)).or_else_raise(
1✔
2338
        lambda: RuntimeException(f"Dynamic Var {NS_VAR_SYM} not bound!")
2339
    )
2340
    try:
1✔
2341
        yield
1✔
2342
    finally:
2343
        ns_var.pop_bindings()
1✔
2344
        logger.debug(f"Reset bindings for {NS_VAR_SYM} to {ns_var.value}")
1✔
2345

2346

2347
def get_current_ns() -> Namespace:
1✔
2348
    """Get the value of the dynamic variable `*ns*` in the current thread."""
2349
    ns: Namespace = (
1✔
2350
        Maybe(Var.find(NS_VAR_SYM))
2351
        .map(lambda v: v.value)
2352
        .or_else_raise(lambda: RuntimeException(f"Dynamic Var {NS_VAR_SYM} not bound!"))
2353
    )
2354
    return ns
1✔
2355

2356

2357
def set_current_ns(
1✔
2358
    ns_name: str,
2359
    module: BasilispModule | None = None,
2360
) -> Var:
2361
    """Set the value of the dynamic variable `*ns*` in the current thread."""
2362
    symbol = sym.symbol(ns_name)
1✔
2363
    ns = Namespace.get_or_create(symbol, module=module)
1✔
2364
    ns_var = Maybe(Var.find(NS_VAR_SYM)).or_else_raise(
1✔
2365
        lambda: RuntimeException(f"Dynamic Var {NS_VAR_SYM} not bound!")
2366
    )
2367
    ns_var.push_bindings(ns)
1✔
2368
    logger.debug(f"Setting {NS_VAR_SYM} to {ns}")
1✔
2369
    return ns_var
1✔
2370

2371

2372
##############################
2373
# Emit Generated Python Code #
2374
##############################
2375

2376

2377
def add_generated_python(
1✔
2378
    generated_python: str,
2379
    which_ns: Namespace | None = None,
2380
) -> None:
2381
    """Add generated Python code to a dynamic variable in which_ns."""
2382
    if which_ns is None:
1✔
2383
        which_ns = get_current_ns()
1✔
2384
    v = Maybe(which_ns.find(sym.symbol(GENERATED_PYTHON_VAR_NAME))).or_else(
1✔
2385
        lambda: Var.intern(
2386
            which_ns,  # type: ignore[arg-type, unused-ignore]
2387
            sym.symbol(GENERATED_PYTHON_VAR_NAME),
2388
            "",
2389
            dynamic=True,
2390
            meta=lmap.map({_PRIVATE_META_KEY: True}),
2391
        )
2392
    )
2393
    # Accessing the Var root via the property uses a lock, which is the
2394
    # desired behavior for Basilisp code, but it introduces additional
2395
    # startup time when there will not realistically be any contention.
2396
    v._root = v._root + generated_python  # type: ignore
1✔
2397

2398

2399
def print_generated_python() -> bool:
1✔
2400
    """Return the value of the `*print-generated-python*` dynamic variable."""
2401
    ns_sym = sym.symbol(PRINT_GENERATED_PY_VAR_NAME, ns=CORE_NS)
1✔
2402
    return (
1✔
2403
        Maybe(Var.find(ns_sym))
2404
        .map(lambda v: v.value)
2405
        .or_else_raise(lambda: RuntimeException(f"Dynamic Var {ns_sym} not bound!"))
2406
    )
2407

2408

2409
#########################
2410
# Bootstrap the Runtime #
2411
#########################
2412

2413

2414
def init_ns_var() -> Var:
1✔
2415
    """Initialize the dynamic `*ns*` variable in the `basilisp.core` Namespace."""
2416
    core_ns = Namespace.get_or_create(CORE_NS_SYM)
1✔
2417
    ns_var = Var.intern(
1✔
2418
        core_ns,
2419
        sym.symbol(NS_VAR_NAME),
2420
        core_ns,
2421
        dynamic=True,
2422
        meta=lmap.map(
2423
            {
2424
                _DOC_META_KEY: (
2425
                    "Pointer to the current namespace.\n\n"
2426
                    "This value is used by both the compiler and runtime to determine where "
2427
                    "newly defined Vars should be bound, so users should not alter or bind "
2428
                    "this Var unless they know what they're doing."
2429
                )
2430
            }
2431
        ),
2432
    )
2433
    logger.debug(f"Created namespace variable {NS_VAR_SYM}")
1✔
2434
    return ns_var
1✔
2435

2436

2437
def bootstrap_core(compiler_opts: CompilerOpts) -> None:
1✔
2438
    """Bootstrap the environment with functions that are either difficult to express
2439
    with the very minimal Lisp environment or which are expected by the compiler."""
2440
    _NS = Maybe(Var.find(NS_VAR_SYM)).or_else_raise(
1✔
2441
        lambda: RuntimeException(f"Dynamic Var {NS_VAR_SYM} not bound!")
2442
    )
2443

2444
    def in_ns(s: sym.Symbol):
1✔
2445
        ns = Namespace.get_or_create(s)
1✔
2446
        _NS.set_value(ns)
1✔
2447
        return ns
1✔
2448

2449
    # Vars used in bootstrapping the runtime
2450
    Var.intern_unbound(
1✔
2451
        CORE_NS_SYM,
2452
        sym.symbol("unquote"),
2453
        meta=lmap.map(
2454
            {
2455
                _DOC_META_KEY: (
2456
                    "Placeholder Var so the compiler does not throw an error while syntax quoting.\n\n"
2457
                    "See :ref:`macros` and :ref:`syntax_quoting` for more details."
2458
                )
2459
            }
2460
        ),
2461
    )
2462
    Var.intern_unbound(
1✔
2463
        CORE_NS_SYM,
2464
        sym.symbol("unquote-splicing"),
2465
        meta=lmap.map(
2466
            {
2467
                _DOC_META_KEY: (
2468
                    "Placeholder Var so the compiler does not throw an error while syntax quoting.\n\n"
2469
                    "See :ref:`macros` and :ref:`syntax_quoting` for more details."
2470
                )
2471
            }
2472
        ),
2473
    )
2474
    Var.intern(
1✔
2475
        CORE_NS_SYM, sym.symbol("in-ns"), in_ns, meta=lmap.map({_REDEF_META_KEY: True})
2476
    )
2477
    Var.intern(
1✔
2478
        CORE_NS_SYM,
2479
        sym.symbol(IMPORT_MODULE_VAR_NAME),
2480
        None,
2481
        dynamic=True,
2482
        meta=lmap.map(
2483
            {
2484
                _DOC_META_KEY: "If not ``nil``, corresponds to the module which is currently being imported."
2485
            }
2486
        ),
2487
    )
2488

2489
    # Dynamic Var examined by the compiler when importing new Namespaces
2490
    Var.intern(
1✔
2491
        CORE_NS_SYM,
2492
        sym.symbol(COMPILER_OPTIONS_VAR_NAME),
2493
        compiler_opts,
2494
        dynamic=True,
2495
    )
2496

2497
    # Dynamic Var containing command line arguments passed via `basilisp run`
2498
    Var.intern(
1✔
2499
        CORE_NS_SYM,
2500
        sym.symbol(COMMAND_LINE_ARGS_VAR_NAME),
2501
        None,
2502
        dynamic=True,
2503
        meta=lmap.map(
2504
            {
2505
                _DOC_META_KEY: (
2506
                    "A vector of command line arguments if this process was started "
2507
                    "with command line arguments as by ``basilisp run {file_or_code}`` "
2508
                    "or ``nil`` otherwise.\n\n"
2509
                    "Note that this value will differ from ``sys.argv`` since it will "
2510
                    "not include the command line arguments consumed by Basilisp's "
2511
                    "own CLI."
2512
                )
2513
            }
2514
        ),
2515
    )
2516

2517
    # Dynamic Var containing command line arguments passed via `basilisp run`
2518
    Var.intern(
2519
        CORE_NS_SYM,
2520
        sym.symbol(MAIN_NS_VAR_NAME),
2521
        None,
2522
        dynamic=True,
2523
        meta=lmap.map(
2524
            {
2525
                _DOC_META_KEY: (
2526
                    "The name of the main namespace as a symbol if this process was "
2527
                    "executed as ``basilisp run -n {namespace}`` or "
2528
                    "``python -m {namespace}`` or ``nil`` otherwise.\n\n"
2529
                    "This can be useful for detecting scripts similarly to how Python "
2530
                    'scripts use the idiom ``if __name__ == "__main__":``.'
2531
                )
2532
            }
2533
        ),
2534
    )
2535

2536
    # Dynamic Var for introspecting the default reader featureset
2537
    Var.intern(
1✔
2538
        CORE_NS_SYM,
2539
        sym.symbol(DEFAULT_READER_FEATURES_VAR_NAME),
2540
        READER_COND_DEFAULT_FEATURE_SET,
2541
        dynamic=True,
2542
        meta=lmap.map(
2543
            {
2544
                _DOC_META_KEY: (
2545
                    "The set of all currently supported "
2546
                    ":ref:`reader features <reader_conditionals>`."
2547
                )
2548
            }
2549
        ),
2550
    )
2551

2552
    # Dynamic Vars examined by the compiler for generating Python code for debugging
2553
    Var.intern(
1✔
2554
        CORE_NS_SYM,
2555
        sym.symbol(PRINT_GENERATED_PY_VAR_NAME),
2556
        False,
2557
        dynamic=True,
2558
        meta=lmap.map({_PRIVATE_META_KEY: True}),
2559
    )
2560
    Var.intern(
1✔
2561
        CORE_NS_SYM,
2562
        sym.symbol(GENERATED_PYTHON_VAR_NAME),
2563
        "",
2564
        dynamic=True,
2565
        meta=lmap.map({_PRIVATE_META_KEY: True}),
2566
    )
2567

2568
    # Dynamic Vars for controlling printing
2569
    Var.intern(
1✔
2570
        CORE_NS_SYM, sym.symbol(PRINT_DUP_VAR_NAME), lobj.PRINT_DUP, dynamic=True
2571
    )
2572
    Var.intern(
1✔
2573
        CORE_NS_SYM,
2574
        sym.symbol(PRINT_LENGTH_VAR_NAME),
2575
        lobj.PRINT_LENGTH,
2576
        dynamic=True,
2577
        meta=lmap.map(
2578
            {
2579
                _DOC_META_KEY: (
2580
                    "Limits the number of items printed per collection. If falsy, all items are shown."
2581
                    " If set to an integer, only that many items are printed,"
2582
                    " with ``...`` indicating more. By default, it is ``nil``, meaning no limit."
2583
                )
2584
            }
2585
        ),
2586
    )
2587
    Var.intern(
1✔
2588
        CORE_NS_SYM, sym.symbol(PRINT_LEVEL_VAR_NAME), lobj.PRINT_LEVEL, dynamic=True
2589
    )
2590
    Var.intern(
1✔
2591
        CORE_NS_SYM, sym.symbol(PRINT_META_VAR_NAME), lobj.PRINT_META, dynamic=True
2592
    )
2593
    Var.intern(
1✔
2594
        CORE_NS_SYM,
2595
        sym.symbol(PRINT_NAMESPACE_MAPS_VAR_NAME),
2596
        lobj.PRINT_NAMESPACE_MAPS,
2597
        dynamic=True,
2598
        meta=lmap.map(
2599
            {
2600
                _DOC_META_KEY: (
2601
                    "Indicates to print the namespace of keys in a map belonging to the same"
2602
                    " namespace, at the beginning of the map instead of beside the keys."
2603
                    " Defaults to false."
2604
                )
2605
            }
2606
        ),
2607
    )
2608
    Var.intern(
1✔
2609
        CORE_NS_SYM,
2610
        sym.symbol(PRINT_READABLY_VAR_NAME),
2611
        lobj.PRINT_READABLY,
2612
        dynamic=True,
2613
    )
2614

2615
    # Version info
2616
    Var.intern(
1✔
2617
        CORE_NS_SYM,
2618
        sym.symbol(PYTHON_VERSION_VAR_NAME),
2619
        vec.vector(sys.version_info),
2620
        dynamic=True,
2621
        meta=lmap.map(
2622
            {
2623
                _DOC_META_KEY: (
2624
                    "The current Python version as a vector of "
2625
                    "``[major, minor, revision]``."
2626
                )
2627
            }
2628
        ),
2629
    )
2630
    Var.intern(
1✔
2631
        CORE_NS_SYM,
2632
        sym.symbol(BASILISP_VERSION_VAR_NAME),
2633
        BASILISP_VERSION,
2634
        dynamic=True,
2635
        meta=lmap.map(
2636
            {
2637
                _DOC_META_KEY: (
2638
                    "The current Basilisp version as a vector of "
2639
                    "``[major, minor, revision]``."
2640
                )
2641
            }
2642
        ),
2643
    )
2644

2645

2646
def get_compiler_opts() -> CompilerOpts:
1✔
2647
    """Return the current compiler options map."""
2648
    v = Var.find_in_ns(CORE_NS_SYM, sym.symbol(COMPILER_OPTIONS_VAR_NAME))
1✔
2649
    assert v is not None, "*compiler-options* Var not defined"
1✔
2650
    return cast(CompilerOpts, v.value)
1✔
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

© 2026 Coveralls, Inc