-
-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathruntime.py
More file actions
2778 lines (2220 loc) · 87.1 KB
/
runtime.py
File metadata and controls
2778 lines (2220 loc) · 87.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# pylint: disable=too-many-lines
import builtins
import collections.abc
import contextlib
import decimal
import functools
import graphlib
import importlib.metadata
import inspect
import itertools
import logging
import math
import numbers
import pickle # nosec B403
import platform
import re
import sys
import threading
import types
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence, Sized
from typing import AbstractSet, Any, NoReturn, Optional, TypeVar, Union, cast
import attr
from basilisp.lang import keyword as kw
from basilisp.lang import list as llist
from basilisp.lang import map as lmap
from basilisp.lang import obj as lobj
from basilisp.lang import seq as lseq
from basilisp.lang import set as lset
from basilisp.lang import symbol as sym
from basilisp.lang import vector as vec
from basilisp.lang.atom import Atom
from basilisp.lang.interfaces import (
IAssociative,
IBlockingDeref,
IDeref,
IIndexed,
ILookup,
IMapEntry,
IPersistentCollection,
IPersistentList,
IPersistentMap,
IPersistentSet,
IPersistentStack,
IPersistentVector,
IReduce,
ISeq,
ISeqable,
ITransientAssociative,
ITransientSet,
ReduceFunction,
)
from basilisp.lang.reduced import Reduced
from basilisp.lang.reference import RefBase, ReferenceBase
from basilisp.lang.typing import BasilispFunction, Comparable, CompilerOpts
from basilisp.lang.util import OBJECT_DUNDER_METHODS, demunge, is_abstract, munge
from basilisp.util import Maybe
logger = logging.getLogger(__name__)
# Public constants
CORE_NS = "basilisp.core"
CORE_NS_SYM = sym.symbol(CORE_NS)
NS_VAR_NAME = "*ns*"
NS_VAR_SYM = sym.symbol(NS_VAR_NAME, ns=CORE_NS)
IMPORT_MODULE_VAR_NAME = "*import-module*"
IMPORT_MODULE_VAR_SYM = sym.symbol(IMPORT_MODULE_VAR_NAME, ns=CORE_NS)
NS_VAR_NS = CORE_NS
REPL_DEFAULT_NS = "basilisp.user"
SUPPORTED_PYTHON_VERSIONS = frozenset({(3, 10), (3, 11), (3, 12), (3, 13), (3, 14)})
BASILISP_VERSION_STRING = importlib.metadata.version("basilisp")
BASILISP_VERSION = vec.vector(
(int(s) if s.isdigit() else s) for s in BASILISP_VERSION_STRING.split(".")
)
# Public basilisp.core symbol names
COMPILER_OPTIONS_VAR_NAME = "*compiler-options*"
COMMAND_LINE_ARGS_VAR_NAME = "*command-line-args*"
DEFAULT_READER_FEATURES_VAR_NAME = "*default-reader-features*"
GENERATED_PYTHON_VAR_NAME = "*generated-python*"
PRINT_GENERATED_PY_VAR_NAME = "*print-generated-python*"
MAIN_NS_VAR_NAME = "*main-ns*"
PRINT_DUP_VAR_NAME = "*print-dup*"
PRINT_LENGTH_VAR_NAME = "*print-length*"
PRINT_LEVEL_VAR_NAME = "*print-level*"
PRINT_META_VAR_NAME = "*print-meta*"
PRINT_NAMESPACE_MAPS_VAR_NAME = "*print-namespace-maps*"
PRINT_READABLY_VAR_NAME = "*print-readably*"
PYTHON_VERSION_VAR_NAME = "*python-version*"
BASILISP_VERSION_VAR_NAME = "*basilisp-version*"
# Common meta keys
_DOC_META_KEY = kw.keyword("doc")
_DYNAMIC_META_KEY = kw.keyword("dynamic")
_PRIVATE_META_KEY = kw.keyword("private")
_REDEF_META_KEY = kw.keyword("redef")
# Special form values, used for resolving Vars
_AWAIT = sym.symbol("await")
_CATCH = sym.symbol("catch")
_DEF = sym.symbol("def")
_DEFTYPE = sym.symbol("deftype*")
_DO = sym.symbol("do")
_FINALLY = sym.symbol("finally")
_FN = sym.symbol("fn*")
_IF = sym.symbol("if")
_IMPORT = sym.symbol("import*")
_INTEROP_CALL = sym.symbol(".")
_INTEROP_PROP = sym.symbol(".-")
_LET = sym.symbol("let*")
_LETFN = sym.symbol("letfn*")
_LOOP = sym.symbol("loop*")
_QUOTE = sym.symbol("quote")
_REIFY = sym.symbol("reify*")
_RECUR = sym.symbol("recur")
_REQUIRE = sym.symbol("require*")
_SET_BANG = sym.symbol("set!")
_THROW = sym.symbol("throw")
_TRY = sym.symbol("try")
_VAR = sym.symbol("var")
_YIELD = sym.symbol("yield")
_SPECIAL_FORMS = lset.s(
_AWAIT,
_CATCH,
_DEF,
_DEFTYPE,
_DO,
_FINALLY,
_FN,
_IF,
_IMPORT,
_INTEROP_CALL,
_INTEROP_PROP,
_LET,
_LETFN,
_LOOP,
_QUOTE,
_RECUR,
_REIFY,
_REQUIRE,
_SET_BANG,
_THROW,
_TRY,
_VAR,
_YIELD,
)
# Reader Conditional default features
def _supported_python_versions_features() -> Iterable[kw.Keyword]:
"""Yield successive reader features corresponding to the various Python
`major`.`minor` versions the current Python VM corresponds to amongst the
set of supported Python versions.
For example, for Python 3.6, we would emit:
- :lpy36 - to exactly match Basilisp running on Python 3.6
- :lpy36+ - to match Basilisp running on Python 3.6 and later versions
- :lpy36- - to match Basilisp running on Python 3.6 and earlier versions
- :lpy37- - to match Basilisp running on Python 3.7 and earlier versions
- :lpy38- - to match Basilisp running on Python 3.8 and earlier versions"""
feature_kw = lambda major, minor, suffix="": kw.keyword(
f"lpy{major}{minor}{suffix}"
)
yield feature_kw(sys.version_info.major, sys.version_info.minor)
current = (sys.version_info.major, sys.version_info.minor)
for version in SUPPORTED_PYTHON_VERSIONS:
if current <= version:
yield feature_kw(version[0], version[1], suffix="-")
if current >= version:
yield feature_kw(version[0], version[1], suffix="+")
READER_COND_BASILISP_FEATURE_KW = kw.keyword("lpy")
READER_COND_DEFAULT_FEATURE_KW = kw.keyword("default")
READER_COND_PLATFORM = kw.keyword(platform.system().lower())
READER_COND_DEFAULT_FEATURE_SET = lset.s(
READER_COND_BASILISP_FEATURE_KW,
READER_COND_DEFAULT_FEATURE_KW,
READER_COND_PLATFORM,
*_supported_python_versions_features(),
)
CompletionMatcher = Callable[[tuple[sym.Symbol, Any]], bool]
CompletionTrimmer = Callable[[tuple[sym.Symbol, Any]], str]
class BasilispModule(types.ModuleType):
__basilisp_namespace__: "Namespace"
__basilisp_bootstrapped__: bool = False
# pylint: disable=attribute-defined-outside-init
def _new_module(name: str, doc=None) -> BasilispModule:
"""Create a new empty Basilisp Python module.
Modules are created for each Namespace when it is created."""
mod = BasilispModule(name, doc=doc)
mod.__loader__ = None
mod.__package__ = None
mod.__spec__ = None
mod.__basilisp_bootstrapped__ = False
return mod
class RuntimeException(Exception):
pass
class _VarBindings(threading.local):
def __init__(self):
self.bindings: list = []
class Unbound:
__slots__ = ("var",)
def __init__(self, v: "Var"):
self.var = v
def __repr__(self): # pragma: no cover
return f"Unbound(var={self.var})"
def __eq__(self, other):
return self is other or (isinstance(other, Unbound) and self.var == other.var)
class Var(RefBase):
__slots__ = (
"_name",
"_ns",
"_root",
"_dynamic",
"_is_bound",
"_tl",
"_meta",
"_lock",
"_watches",
"_validator",
)
def __init__(
self,
ns: "Namespace",
name: sym.Symbol,
dynamic: bool = False,
meta: IPersistentMap | None = None,
) -> None:
self._ns = ns
self._name = name
self._root = Unbound(self)
self._dynamic = dynamic
self._is_bound = False
self._tl = None
self._meta = meta
self._lock = threading.RLock()
self._watches = lmap.EMPTY
self._validator = None
if dynamic:
self._tl = _VarBindings()
# If this var was created with the dynamic keyword argument, then the
# Var metadata should also specify that the Var is dynamic.
if isinstance(self._meta, lmap.PersistentMap):
if not self._meta.val_at(_DYNAMIC_META_KEY):
self._meta = self._meta.assoc(_DYNAMIC_META_KEY, True)
else:
self._meta = lmap.map({_DYNAMIC_META_KEY: True})
def __repr__(self):
return f"#'{self.ns.name}/{self.name}"
def __call__(self, *args, **kwargs):
return self.value(*args, *kwargs) # pylint: disable=not-callable
@property
def ns(self) -> "Namespace":
return self._ns
@property
def name(self) -> sym.Symbol:
return self._name
@property
def dynamic(self) -> bool:
return self._dynamic
def set_dynamic(self, dynamic: bool) -> None:
with self._lock:
if dynamic == self._dynamic:
return
self._dynamic = dynamic
self._tl = _VarBindings() if dynamic else None
@property
def is_private(self) -> bool | None:
if self._meta is not None:
return self._meta.val_at(_PRIVATE_META_KEY)
return False
@property
def is_bound(self) -> bool:
return self._is_bound or self.is_thread_bound
def _set_root(self, newval) -> None:
# Private setter which does not include lock so it can be used
# by other properties and methods which already manage the lock.
oldval = self._root
self._validate(newval)
self._is_bound = True
self._root = newval
self._notify_watches(oldval, newval)
return newval
@property
def root(self):
with self._lock:
return self._root
def bind_root(self, val) -> None:
"""Atomically update the root binding of this Var to val."""
with self._lock:
self._set_root(val)
def alter_root(self, f, *args) -> None:
"""Atomically alter the root binding of this Var to the result of calling
f with the existing root value and any additional arguments. Returns the
new value."""
with self._lock:
return self._set_root(f(self._root, *args))
def push_bindings(self, val):
if not self._dynamic or self._tl is None:
raise RuntimeException("Can only push bindings to dynamic Vars")
self._validate(val)
self._tl.bindings.append(val)
def pop_bindings(self):
if not self._dynamic or self._tl is None:
raise RuntimeException("Can only pop bindings from dynamic Vars")
return self._tl.bindings.pop()
def deref(self):
return self.value
@property
def is_thread_bound(self):
return bool(self._dynamic and self._tl and self._tl.bindings)
@property
def value(self):
"""Return the current value of the Var visible in the current thread.
For non-dynamic Vars, this will just be the root. For dynamic Vars, this will
be any thread-local binding if one is defined. Otherwise, the root value."""
with self._lock:
if self._dynamic:
assert self._tl is not None
if len(self._tl.bindings) > 0:
return self._tl.bindings[-1]
return self._root
def set_value(self, v) -> None:
"""Set the current value of the Var.
If the Var is not dynamic, this is equivalent to binding the root value. If the
Var is dynamic, this will set the thread-local bindings for the Var."""
with self._lock:
if self._dynamic:
assert self._tl is not None
self._validate(v)
if len(self._tl.bindings) > 0:
self._tl.bindings[-1] = v
else:
self.push_bindings(v)
return
self._set_root(v)
__UNBOUND_SENTINEL = object()
@classmethod
def intern( # pylint: disable=too-many-arguments
cls,
ns: Union["Namespace", sym.Symbol],
name: sym.Symbol,
val,
dynamic: bool = False,
meta: IPersistentMap | None = None,
) -> "Var":
"""Intern the value bound to the symbol `name` in namespace `ns`.
If the Var already exists, it will have its root value updated to `val`,
its meta will be reset to match the provided `meta`, and the dynamic flag
will be updated to match the provided `dynamic` argument."""
if isinstance(ns, sym.Symbol):
ns = Namespace.get_or_create(ns)
new_var = cls(ns, name, dynamic=dynamic, meta=meta)
var = ns.intern(name, new_var)
if val is not cls.__UNBOUND_SENTINEL:
var.bind_root(val)
# Namespace.intern will return an existing interned Var for the same name
# if one exists. We only want to set the meta and/or dynamic flag if the
# Var is not new.
if var is not new_var:
var.reset_meta(meta)
var.set_dynamic(dynamic)
return var
@classmethod
def intern_unbound(
cls,
ns: Union["Namespace", sym.Symbol],
name: sym.Symbol,
dynamic: bool = False,
meta: IPersistentMap | None = None,
) -> "Var":
"""Create a new unbound `Var` instance to the symbol `name` in namespace `ns`."""
return cls.intern(ns, name, cls.__UNBOUND_SENTINEL, dynamic=dynamic, meta=meta)
@staticmethod
def find_in_ns(
ns_or_sym: Union["Namespace", sym.Symbol], name_sym: sym.Symbol
) -> "Optional[Var]":
"""Return the value current bound to the name `name_sym` in the namespace
specified by `ns_sym`."""
ns = (
Namespace.get(ns_or_sym) if isinstance(ns_or_sym, sym.Symbol) else ns_or_sym
)
if ns is not None:
return ns.find(name_sym)
return None
@classmethod
def find(cls, ns_qualified_sym: sym.Symbol) -> "Optional[Var]":
"""Return the value currently bound to the name in the namespace specified
by `ns_qualified_sym`."""
ns = Maybe(ns_qualified_sym.ns).or_else_raise(
lambda: ValueError(
f"Namespace must be specified in Symbol {ns_qualified_sym}"
)
)
ns_sym = sym.symbol(ns)
name_sym = sym.symbol(ns_qualified_sym.name)
return cls.find_in_ns(ns_sym, name_sym)
@classmethod
def find_safe(cls, ns_qualified_sym: sym.Symbol) -> "Var":
"""Return the Var currently bound to the name in the namespace specified
by `ns_qualified_sym`. If no Var is bound to that name, raise an exception.
This is a utility method to return useful debugging information when code
refers to an invalid symbol at runtime."""
v = cls.find(ns_qualified_sym)
if v is None:
raise RuntimeException(
f"Unable to resolve symbol {ns_qualified_sym} in this context"
)
return v
Frame = IPersistentSet[Var]
FrameStack = IPersistentStack[Frame]
class _ThreadBindings(threading.local):
def __init__(self):
self._bindings: FrameStack = vec.EMPTY
def get_bindings(self) -> FrameStack:
return self._bindings
def push_bindings(self, frame: Frame) -> None:
self._bindings = self._bindings.cons(frame)
def pop_bindings(self) -> Frame:
frame = self._bindings.peek()
self._bindings = self._bindings.pop()
assert frame is not None
return frame
_THREAD_BINDINGS = _ThreadBindings()
@attr.frozen
class ImportRefer:
module_name: sym.Symbol
value: Any
AliasMap = lmap.PersistentMap[sym.Symbol, sym.Symbol]
ImportReferMap = lmap.PersistentMap[sym.Symbol, Any]
Module = Union[BasilispModule, types.ModuleType]
ModuleMap = lmap.PersistentMap[sym.Symbol, Module]
NamespaceMap = lmap.PersistentMap[sym.Symbol, "Namespace"]
VarMap = lmap.PersistentMap[sym.Symbol, Var]
_PRIVATE_NAME_PATTERN = re.compile(r"(_\w*|__\w+__)")
class Namespace(ReferenceBase):
"""Namespaces serve as organizational units in Basilisp code, just as they do in
Clojure code.
Vars are mutable containers for functions and data which may be interned in a
namespace and referred to by a Symbol.
Namespaces additionally may have aliases to other namespaces, so code organized in
one namespace may conveniently refer to code or data in other namespaces using that
alias as the Symbol's namespace.
Namespaces are constructed def-by-def as Basilisp reads in each form in a file
(which will typically declare a namespace at the top).
Namespaces have the following fields of interest:
- `aliases` is a mapping between a symbolic alias and another Namespace. The fully
qualified name of a namespace is also an alias for itself.
- `imports` is a mapping of names to Python modules imported into the current
namespace.
- `import_aliases` is a mapping of aliases for Python modules to the true module
name.
- `interns` is a mapping between a symbolic name and a Var. The Var may point to
code, data, or nothing, if it is unbound. Vars in `interns` are interned in
_this_ namespace.
- `refers` is a mapping between a symbolic name and a Var. Vars in `refers` are
interned in another namespace and are only referred to without an alias in
this namespace.
"""
# If this set is updated, be sure to update the following two locations:
# - basilisp.lang.compiler.generator._MODULE_ALIASES
# - the `namespace_imports` section in the documentation
DEFAULT_IMPORTS = lset.set(
map(
sym.symbol,
[
"attr",
"builtins",
"functools",
"io",
"importlib",
"operator",
"sys",
"basilisp.lang.atom",
"basilisp.lang.compiler",
"basilisp.lang.delay",
"basilisp.lang.exception",
"basilisp.lang.futures",
"basilisp.lang.interfaces",
"basilisp.lang.keyword",
"basilisp.lang.list",
"basilisp.lang.map",
"basilisp.lang.multifn",
"basilisp.lang.numbers",
"basilisp.lang.promise",
"basilisp.lang.queue",
"basilisp.lang.reader",
"basilisp.lang.reduced",
"basilisp.lang.runtime",
"basilisp.lang.seq",
"basilisp.lang.set",
"basilisp.lang.symbol",
"basilisp.lang.tagged",
"basilisp.lang.vector",
"basilisp.lang.volatile",
"basilisp.lang.util",
],
)
)
_NAMESPACES: Atom[NamespaceMap] = Atom(lmap.EMPTY)
__slots__ = (
"_name",
"_module",
"_meta",
"_lock",
"_interns",
"_refers",
"_aliases",
"_imports",
"_import_aliases",
"_import_refers",
)
def __init__(self, name: sym.Symbol, module: BasilispModule | None = None) -> None:
self._name = name
self._module = Maybe(module).or_else(lambda: _new_module(name.as_python_sym()))
self._meta: IPersistentMap | None = None
self._lock = threading.RLock()
self._aliases: NamespaceMap = lmap.EMPTY
self._imports: ModuleMap = lmap.map(
dict(
map(
lambda s: (s, importlib.import_module(s.name)),
Namespace.DEFAULT_IMPORTS,
)
)
)
self._import_aliases: AliasMap = lmap.EMPTY
self._import_refers: lmap.PersistentMap[sym.Symbol, ImportRefer] = lmap.EMPTY
self._interns: VarMap = lmap.EMPTY
self._refers: VarMap = lmap.EMPTY
@property
def name(self) -> str:
return self._name.name
@property
def module(self) -> BasilispModule:
return self._module
@module.setter
def module(self, m: BasilispModule):
"""Override the Python module for this Namespace.
***WARNING**
This should only be done by basilisp.importer code to make sure the
correct module is generated for `basilisp.core`."""
self._module = m
@property
def aliases(self) -> NamespaceMap:
"""A mapping between a symbolic alias and another Namespace. The
fully qualified name of a namespace is also an alias for itself."""
with self._lock:
return self._aliases
@property
def imports(self) -> ModuleMap:
"""A mapping of names to Python modules imported into the current
namespace."""
with self._lock:
return self._imports
@property
def import_aliases(self) -> AliasMap:
"""A mapping of a symbolic alias and a Python module name."""
with self._lock:
return self._import_aliases
@property
def import_refers(self) -> ImportReferMap:
"""A mapping of a symbolic alias and a Python object from an imported module."""
with self._lock:
return lmap.map({name: v.value for name, v in self._import_refers.items()})
@property
def interns(self) -> VarMap:
"""A mapping between a symbolic name and a Var. The Var may point to
code, data, or nothing, if it is unbound. Vars in `interns` are
interned in _this_ namespace."""
with self._lock:
return self._interns
@property
def refers(self) -> VarMap:
"""A mapping between a symbolic name and a Var. Vars in refers are
interned in another namespace and are only referred to without an
alias in this namespace."""
with self._lock:
return self._refers
def __repr__(self):
return f"{self._name}"
def __hash__(self):
return hash(self._name)
def _get_required_namespaces(self) -> vec.PersistentVector["Namespace"]:
"""Return a vector of all required namespaces (loaded via `require`, `use`,
or `refer`).
This vector will include `basilisp.core` unless the namespace was created
manually without requiring it."""
ts: graphlib.TopologicalSorter = graphlib.TopologicalSorter()
def add_nodes(ns: Namespace) -> None:
# basilisp.core does actually create a cycle by requiring namespaces
# that require it, so we cannot add it to the topological sorter here,
# or it will throw a cycle error
if ns.name == CORE_NS:
return
for aliased_ns in ns.aliases.values():
ts.add(ns, aliased_ns)
add_nodes(aliased_ns)
for referred_var in ns.refers.values():
referred_ns = referred_var.ns
ts.add(ns, referred_ns)
add_nodes(referred_ns)
add_nodes(self)
return vec.vector(ts.static_order())
def reload_all(self) -> "Namespace":
"""Reload all dependency namespaces as by `Namespace.reload()`."""
sorted_reload_order = self._get_required_namespaces()
logger.debug(f"Computed :reload-all order: {sorted_reload_order}")
for ns in sorted_reload_order:
ns.reload()
return self
def reload(self) -> "Namespace":
"""Reload code in this namespace by reloading the underlying Python module."""
with self._lock:
importlib.reload(self.module)
return self
def require(self, ns_name: str, *aliases: sym.Symbol) -> BasilispModule:
"""Require the Basilisp Namespace named by `ns_name` and add any aliases given
to this Namespace.
This method is called in code generated for the `require*` special form."""
try:
ns_module = importlib.import_module(munge(ns_name))
except ModuleNotFoundError as e:
# Dynamically created namespaces won't have any on-disk modules to
# import, so loading them via the importer will fail. We can check
# for such a namespace here and return the generated module instead.
if (maybe_ns := self.get(sym.symbol(ns_name))) is not None:
ns_module = maybe_ns.module
else:
raise ImportError(
f"Basilisp namespace '{ns_name}' not found",
) from e
assert isinstance(ns_module, BasilispModule)
ns_sym = sym.symbol(ns_name)
ns = self.get(ns_sym)
assert ns is not None, "Namespace must exist after being required"
self.add_alias(ns, ns_sym)
if aliases:
self.add_alias(ns, *aliases)
return ns_module
def add_alias(self, namespace: "Namespace", *aliases: sym.Symbol) -> None:
"""Add Symbol aliases for the given Namespace."""
with self._lock:
new_m = self._aliases
for alias in aliases:
new_m = new_m.assoc(alias, namespace)
self._aliases = new_m
def get_alias(self, alias: sym.Symbol) -> "Optional[Namespace]":
"""Get the Namespace aliased by Symbol or None if it does not exist."""
with self._lock:
return self._aliases.val_at(alias, None)
def remove_alias(self, alias: sym.Symbol) -> None:
"""Remove the Namespace aliased by Symbol. Return None."""
with self._lock:
self._aliases = self._aliases.dissoc(alias)
def intern(self, sym: sym.Symbol, var: Var, force: bool = False) -> Var:
"""Intern the Var given in this namespace mapped by the given Symbol.
If the Symbol already maps to a Var, this method _will not overwrite_
the existing Var mapping unless the force keyword argument is given
and is True."""
with self._lock:
old_var = self._interns.val_at(sym, None)
if old_var is None or force:
self._interns = self._interns.assoc(sym, var)
return self._interns.val_at(sym)
def unmap(self, sym: sym.Symbol) -> None:
with self._lock:
self._interns = self._interns.dissoc(sym)
def find(self, sym: sym.Symbol) -> Var | None:
"""Find Vars mapped by the given Symbol input or None if no Vars are
mapped by that Symbol."""
with self._lock:
v = self._interns.val_at(sym, None)
if v is None:
return self._refers.val_at(sym, None)
return v
def add_import(
self,
sym: sym.Symbol,
module: Module,
*aliases: sym.Symbol,
refers: dict[sym.Symbol, Any] | None = None,
) -> None:
"""Add the Symbol as an imported Symbol in this Namespace.
If aliases are given, the aliases will be associated to the module as well.
If a dictionary of refers is provided, add the referred names as import refers.
"""
with self._lock:
self._imports = self._imports.assoc(sym, module)
if refers:
final_refers = self._import_refers
for s, v in refers.items():
# Filter out dunder names and private names
if _PRIVATE_NAME_PATTERN.fullmatch(s.name) is not None:
logger.debug(f"Ignoring import refer for {sym} member {s}")
continue
final_refers = final_refers.assoc(s, ImportRefer(sym, v))
self._import_refers = final_refers
if aliases:
m = self._import_aliases
for alias in aliases:
m = m.assoc(alias, sym)
self._import_aliases = m
def get_import_refer(self, sym: sym.Symbol) -> sym.Symbol | None:
"""Get the Python module member name referred by Symbol or None if it does not
exist."""
with self._lock:
refer = self._import_refers.val_at(sym, None)
return refer.module_name if refer is not None else None
def get_import(self, sym: sym.Symbol) -> BasilispModule | None:
"""Return the module if a module named by sym has been imported into
this Namespace, None otherwise.
First try to resolve a module directly with the given name. If no module
can be resolved, attempt to resolve the module using import aliases."""
with self._lock:
mod = self._imports.val_at(sym, None)
if mod is None:
alias = self._import_aliases.get(sym, None)
if alias is None:
return None
return self._imports.val_at(alias, None)
return mod
def add_refer(self, sym: sym.Symbol, var: Var) -> None:
"""Refer var in this namespace under the name sym."""
if not var.is_private:
with self._lock:
self._refers = self._refers.assoc(sym, var)
def get_refer(self, sym: sym.Symbol) -> Var | None:
"""Get the Var referred by Symbol or None if it does not exist."""
with self._lock:
return self._refers.val_at(sym, None)
def refer_all(self, other_ns: "Namespace") -> None:
"""Refer all the Vars in the other namespace."""
with self._lock:
final_refers = self._refers
for s, var in other_ns.interns.items():
if not var.is_private:
final_refers = final_refers.assoc(s, var)
self._refers = final_refers
@classmethod
def ns_cache(cls) -> lmap.PersistentMap:
"""Return a snapshot of the Namespace cache."""
return cls._NAMESPACES.deref()
@staticmethod
def __get_or_create(
ns_cache: NamespaceMap,
name: sym.Symbol,
module: BasilispModule | None = None,
) -> lmap.PersistentMap:
"""Private swap function used by `get_or_create` to atomically swap
the new namespace map into the global cache."""
ns = ns_cache.val_at(name, None)
if ns is not None:
return ns_cache
new_ns = Namespace(name, module=module)
# If this is a new namespace and we're given a module (typically from the
# importer), attach the namespace to the module.
if module is not None:
logger.debug(f"Setting module '{module}' namespace to '{new_ns}'")
module.__basilisp_namespace__ = new_ns
# The `ns` macro is important for setting up a new namespace, but it becomes
# available only after basilisp.core has been loaded.
ns_var = Var.find_in_ns(CORE_NS_SYM, sym.symbol("ns"))
if ns_var:
new_ns.add_refer(sym.symbol("ns"), ns_var)
return ns_cache.assoc(name, new_ns)
@classmethod
def get_or_create(
cls, name: sym.Symbol, module: BasilispModule | None = None
) -> "Namespace":
"""Get the namespace bound to the symbol `name` in the global namespace
cache, creating it if it does not exist.
Return the namespace."""
if (
module is None
and (module_var := Var.find(IMPORT_MODULE_VAR_SYM)) is not None
):
module = module_var.value
assert module is None or isinstance(module, BasilispModule)
return cls._NAMESPACES.swap(Namespace.__get_or_create, name, module=module)[
name
]
@classmethod
def get(cls, name: sym.Symbol) -> "Optional[Namespace]":
"""Get the namespace bound to the symbol `name` in the global namespace
cache. Return the namespace if it exists or None otherwise.."""
return cls._NAMESPACES.deref().val_at(name, None)
@classmethod
def remove(cls, name: sym.Symbol) -> Optional["Namespace"]:
"""Remove the namespace bound to the symbol `name` in the global
namespace cache and return that namespace.
Return None if the namespace did not exist in the cache."""
if name == CORE_NS_SYM:
raise ValueError("Cannot remove the Basilisp core namespace")
while True:
oldval: lmap.PersistentMap = cls._NAMESPACES.deref()
ns: Namespace | None = oldval.val_at(name, None)
newval = oldval
if ns is not None:
newval = oldval.dissoc(name)
if cls._NAMESPACES.compare_and_set(oldval, newval):
return ns
# REPL Completion support
@staticmethod
def __completion_matcher(text: str) -> CompletionMatcher:
"""Return a function which matches any symbol keys from map entries
against the given text."""
def is_match(entry: tuple[sym.Symbol, Any]) -> bool:
return entry[0].name.startswith(text)
return is_match
def __complete_alias(
self, prefix: str, name_in_ns: str | None = None
) -> Iterable[str]:
"""Return an iterable of possible completions matching the given
prefix from the list of aliased namespaces. If name_in_ns is given,
further attempt to refine the list to matching names in that namespace."""
candidates = filter(
Namespace.__completion_matcher(prefix),
((s, n) for s, n in self.aliases.items()),
)
if name_in_ns is not None:
for _, candidate_ns in candidates:
for match in candidate_ns.__complete_interns(
name_in_ns, include_private_vars=False
):
yield f"{prefix}/{match}"
else:
for alias, _ in candidates:
yield f"{alias}/"
def __complete_imports_and_aliases(
self, prefix: str, name_in_module: str | None = None
) -> Iterable[str]:
"""Return an iterable of possible completions matching the given
prefix from the list of imports and aliased imports. If name_in_module
is given, further attempt to refine the list to matching names in that
namespace."""
imports = self.imports
aliases = lmap.map(
{
alias: imports.val_at(import_name)
for alias, import_name in self.import_aliases.items()
}
)
candidates = filter(
Namespace.__completion_matcher(prefix),
itertools.chain(
aliases.items(), imports.items(), [(sym.symbol("python"), builtins)]
),
)
if name_in_module is not None:
for _, module in candidates:
for name in module.__dict__:
if name.startswith(name_in_module):
yield f"{prefix}/{name}"
else:
for candidate_name, _ in candidates:
yield f"{candidate_name}/"
def __complete_interns(