Skip to content

Commit 26bc0db

Browse files
authored
Add the ILookup interface (#410)
This commit adds the `ILookup` interface, which is inserted into the `IAssociative` hierarchy. `ILookup` adds `ILookup.val_at`, which returns only values from associative objects and `IAssociative.entry` is amended to return an `IMapEntry`. For #409 we need to apply the `ILookup` interface to `ReaderConditional` types. Before, Basilisp only had the `IAssociative.entry` which was always meant to return an `IMapEntry` (rather than merely a value, as it was doing).
1 parent 59261e0 commit 26bc0db

File tree

14 files changed

+155
-114
lines changed

14 files changed

+155
-114
lines changed

src/basilisp/core.lpy

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3376,7 +3376,14 @@
33763376
(~'contains [~this-gs ~key-gs]
33773377
(or (~field-kw-set ~key-gs)
33783378
(contains? ~'_recmap ~key-gs)))
3379-
(~'entry [~this-gs ~key-gs ~'& args#]
3379+
(~'entry [~this-gs ~key-gs]
3380+
(cond
3381+
(contains? ~field-kw-set ~key-gs)
3382+
(map-entry ~key-gs (python/getattr ~this-gs (munge ~key-gs)))
3383+
3384+
(contains? ~'_recmap ~key-gs)
3385+
(map-entry ~key-gs (get ~'_recmap ~key-gs))))
3386+
(~'val-at [~this-gs ~key-gs ~'& args#]
33803387
(let [[default#] args#]
33813388
(cond
33823389
(contains? ~field-kw-set ~key-gs)

src/basilisp/lang/compiler/analyzer.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def _warn_unused_names(self):
236236
code_loc = (
237237
Maybe(entry.symbol.meta)
238238
.map(
239-
lambda m: f": {m.entry(reader.READER_LINE_KW)}" # type: ignore
239+
lambda m: f": {m.val_at(reader.READER_LINE_KW)}" # type: ignore
240240
)
241241
.or_else_get("")
242242
)
@@ -312,22 +312,22 @@ def filename(self) -> str:
312312
@property
313313
def warn_on_unused_names(self) -> bool:
314314
"""If True, warn when local names are unused."""
315-
return self._opts.entry(WARN_ON_UNUSED_NAMES, True)
315+
return self._opts.val_at(WARN_ON_UNUSED_NAMES, True)
316316

317317
@property
318318
def warn_on_shadowed_name(self) -> bool:
319319
"""If True, warn when a name is shadowed in an inner scope.
320320
321321
Implies warn_on_shadowed_var."""
322-
return self._opts.entry(WARN_ON_SHADOWED_NAME, False)
322+
return self._opts.val_at(WARN_ON_SHADOWED_NAME, False)
323323

324324
@property
325325
def warn_on_shadowed_var(self) -> bool:
326326
"""If True, warn when a def'ed Var name is shadowed in an inner scope.
327327
328328
Implied by warn_on_shadowed_name. The value of warn_on_shadowed_name
329329
supersedes the value of this flag."""
330-
return self.warn_on_shadowed_name or self._opts.entry(
330+
return self.warn_on_shadowed_name or self._opts.val_at(
331331
WARN_ON_SHADOWED_VAR, False
332332
)
333333

@@ -411,7 +411,7 @@ def put_new_symbol( # pylint: disable=too-many-arguments
411411
) and self.warn_on_shadowed_var:
412412
if self.current_ns.find(s) is not None:
413413
logger.warning(f"name '{s}' shadows def'ed Var from outer scope")
414-
if s.meta is not None and s.meta.entry(SYM_NO_WARN_WHEN_UNUSED_META_KEY, None):
414+
if s.meta is not None and s.meta.val_at(SYM_NO_WARN_WHEN_UNUSED_META_KEY, None):
415415
warn_if_unused = False
416416
st.new_symbol(s, binding, warn_if_unused=warn_if_unused)
417417

@@ -454,7 +454,7 @@ def _meta_getter(meta_kw: kw.Keyword) -> MetaGetter:
454454

455455
def has_meta_prop(o: Union[IMeta, Var]) -> bool:
456456
return ( # type: ignore
457-
Maybe(o.meta).map(lambda m: m.entry(meta_kw, None)).or_else_get(False)
457+
Maybe(o.meta).map(lambda m: m.val_at(meta_kw, None)).or_else_get(False)
458458
)
459459

460460
return has_meta_prop
@@ -632,7 +632,7 @@ def _def_ast( # pylint: disable=too-many-branches,too-many-locals
632632
# where we directly set the Var meta for the running Basilisp instance
633633
# this causes problems since we'll end up getting something like
634634
# `(quote ([] [v]))` rather than simply `([] [v])`.
635-
arglists_meta = def_meta.entry(ARGLISTS_KW) # type: ignore
635+
arglists_meta = def_meta.val_at(ARGLISTS_KW) # type: ignore
636636
if isinstance(arglists_meta, llist.List):
637637
assert arglists_meta.first == SpecialForm.QUOTE
638638
var_meta = def_meta.update( # type: ignore
@@ -649,7 +649,7 @@ def _def_ast( # pylint: disable=too-many-branches,too-many-locals
649649
var = Var.intern_unbound(
650650
ns_sym,
651651
bare_name,
652-
dynamic=def_meta.entry(SYM_DYNAMIC_META_KEY, False), # type: ignore
652+
dynamic=def_meta.val_at(SYM_DYNAMIC_META_KEY, False), # type: ignore
653653
meta=var_meta,
654654
)
655655
descriptor = Def(
@@ -1176,7 +1176,7 @@ def _deftype_ast( # pylint: disable=too-many-branches
11761176
field_default = (
11771177
Maybe(field.meta)
11781178
.map(
1179-
lambda m: m.entry( # type: ignore
1179+
lambda m: m.val_at( # type: ignore
11801180
SYM_DEFAULT_META_KEY, __DEFTYPE_DEFAULT_SENTINEL
11811181
)
11821182
)
@@ -1594,12 +1594,12 @@ def _import_ast( # pylint: disable=too-many-branches
15941594
raise AnalyzerException(
15951595
"import alias must take the form: [module :as alias]", form=f
15961596
)
1597-
module_name = f.entry(0)
1597+
module_name = f.val_at(0)
15981598
if not isinstance(module_name, sym.Symbol):
15991599
raise AnalyzerException("Python module name must be a symbol", form=f)
1600-
if not AS == f.entry(1):
1600+
if not AS == f.val_at(1):
16011601
raise AnalyzerException("expected :as alias for Python import", form=f)
1602-
module_alias_sym = f.entry(2)
1602+
module_alias_sym = f.val_at(2)
16031603
if not isinstance(module_alias_sym, sym.Symbol):
16041604
raise AnalyzerException("Python module alias must be a symbol", form=f)
16051605
module_alias = module_alias_sym.name

src/basilisp/lang/compiler/generator.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,13 @@ def filename(self) -> str:
209209
@property
210210
def use_var_indirection(self) -> bool:
211211
"""If True, compile all variable references using Var.find indirection."""
212-
return self._opts.entry(USE_VAR_INDIRECTION, False)
212+
return self._opts.val_at(USE_VAR_INDIRECTION, False)
213213

214214
@property
215215
def warn_on_var_indirection(self) -> bool:
216216
"""If True, warn when a Var reference cannot be direct linked (iff
217217
use_var_indirection is False).."""
218-
return not self.use_var_indirection and self._opts.entry(
218+
return not self.use_var_indirection and self._opts.val_at(
219219
WARN_ON_VAR_INDIRECTION, True
220220
)
221221

@@ -588,7 +588,7 @@ def __should_warn_on_redef(
588588
ctx: GeneratorContext, defsym: sym.Symbol, safe_name: str, def_meta: lmap.Map
589589
) -> bool:
590590
"""Return True if the compiler should emit a warning about this name being redefined."""
591-
no_warn_on_redef = def_meta.entry(SYM_NO_WARN_ON_REDEF_META_KEY, False)
591+
no_warn_on_redef = def_meta.val_at(SYM_NO_WARN_ON_REDEF_META_KEY, False)
592592
if no_warn_on_redef:
593593
return False
594594
elif safe_name in ctx.current_ns.module.__dict__:
@@ -597,7 +597,7 @@ def __should_warn_on_redef(
597597
var = ctx.current_ns.find(defsym)
598598
assert var is not None, f"Var {defsym} cannot be none here"
599599

600-
if var.meta is not None and var.meta.entry(SYM_REDEF_META_KEY):
600+
if var.meta is not None and var.meta.val_at(SYM_REDEF_META_KEY):
601601
return False
602602
elif var.is_bound:
603603
return True
@@ -652,7 +652,7 @@ def _def_to_py_ast( # pylint: disable=too-many-branches
652652

653653
# If the Var is marked as dynamic, we need to generate a keyword argument
654654
# for the generated Python code to set the Var as dynamic
655-
is_dynamic = def_meta.entry(SYM_DYNAMIC_META_KEY, False)
655+
is_dynamic = def_meta.val_at(SYM_DYNAMIC_META_KEY, False)
656656
dynamic_kwarg = (
657657
[ast.keyword(arg="dynamic", value=ast.Constant(is_dynamic))]
658658
if is_dynamic

src/basilisp/lang/interfaces.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
T = TypeVar("T")
88

99

10-
class IDeref(Generic[T]):
10+
class IDeref(Generic[T], ABC):
1111
__slots__ = ()
1212

1313
@abstractmethod
@@ -29,7 +29,7 @@ def deref(
2929
# Python 3.6 and 3.7, which affects a few simple test assertions.
3030
# Since there is little benefit to this type being Generic, I'm leaving
3131
# it as is for now.
32-
class IExceptionInfo(Exception):
32+
class IExceptionInfo(Exception, ABC):
3333
__slots__ = ()
3434

3535
@property
@@ -42,7 +42,7 @@ def data(self) -> "IPersistentMap":
4242
V = TypeVar("V")
4343

4444

45-
class IMapEntry(Generic[K, V]):
45+
class IMapEntry(Generic[K, V], ABC):
4646
__slots__ = ()
4747

4848
@property
@@ -80,6 +80,14 @@ def seq(self) -> "ISeq[T]":
8080
raise NotImplementedError()
8181

8282

83+
class ILookup(Generic[K, V], ABC):
84+
__slots__ = ()
85+
86+
@abstractmethod
87+
def val_at(self, k: K, default: Optional[V] = None) -> Optional[V]:
88+
raise NotImplementedError()
89+
90+
8391
class IPersistentCollection(ISeqable[T]):
8492
__slots__ = ()
8593

@@ -93,7 +101,9 @@ def empty() -> "IPersistentCollection[T]":
93101
raise NotImplementedError()
94102

95103

96-
class IAssociative(Mapping[K, V], IPersistentCollection[IMapEntry[K, V]]):
104+
class IAssociative(
105+
ILookup[K, V], Mapping[K, V], IPersistentCollection[IMapEntry[K, V]]
106+
):
97107
__slots__ = ()
98108

99109
@abstractmethod
@@ -105,7 +115,7 @@ def contains(self, k: K) -> bool:
105115
raise NotImplementedError()
106116

107117
@abstractmethod
108-
def entry(self, k: K, default: Optional[V] = None) -> Optional[V]:
118+
def entry(self, k: K) -> Optional[IMapEntry[K, V]]:
109119
raise NotImplementedError()
110120

111121

src/basilisp/lang/keyword.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def __hash__(self):
3636

3737
def __call__(self, m: IAssociative, default=None):
3838
try:
39-
return m.entry(self, default)
39+
return m.val_at(self, default)
4040
except AttributeError:
4141
return None
4242

src/basilisp/lang/map.py

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,20 @@
11
from builtins import map as pymap
2-
from typing import Callable, Dict, Iterable, Mapping, Sequence, TypeVar, Union
2+
from typing import Callable, Dict, Iterable, Mapping, TypeVar, Union
33

4-
from pyrsistent import ( # noqa # pylint: disable=unused-import
5-
PMap,
6-
PVector,
7-
pmap,
8-
pvector,
9-
)
4+
from pyrsistent import PMap, pmap # noqa # pylint: disable=unused-import
105

116
from basilisp.lang.interfaces import ILispObject, IMapEntry, IMeta, IPersistentMap, ISeq
127
from basilisp.lang.obj import map_lrepr as _map_lrepr
138
from basilisp.lang.seq import sequence
14-
from basilisp.lang.vector import Vector
9+
from basilisp.lang.vector import MapEntry, Vector
1510
from basilisp.util import partition
1611

1712
T = TypeVar("T")
1813
K = TypeVar("K")
1914
V = TypeVar("V")
2015

2116

22-
class MapEntry(IMapEntry[K, V], Vector[Union[K, V]]):
23-
__slots__ = ()
24-
25-
def __init__(self, wrapped: "PVector[Union[K, V]]") -> None:
26-
try:
27-
if not len(wrapped) == 2:
28-
raise ValueError("Vector arg to map conj must be a pair")
29-
except TypeError as e:
30-
raise TypeError(f"Cannot make map entry from {type(wrapped)}") from e
31-
32-
super().__init__(wrapped)
33-
34-
@property
35-
def key(self) -> K:
36-
return self[0]
37-
38-
@property
39-
def value(self) -> V:
40-
return self[1]
41-
42-
@staticmethod
43-
def of(k: K, v: V) -> "MapEntry[K, V]":
44-
return MapEntry(pvector([k, v]))
45-
46-
@staticmethod
47-
def from_vec(v: Sequence[Union[K, V]]) -> "MapEntry[K, V]":
48-
return MapEntry(pvector(v))
17+
_ENTRY_SENTINEL = object()
4918

5019

5120
class Map(ILispObject, IMeta, IPersistentMap[K, V]):
@@ -126,7 +95,13 @@ def dissoc(self, *ks):
12695
pass
12796
return Map(m.persistent())
12897

129-
def entry(self, k, default=None):
98+
def entry(self, k):
99+
v = self._inner.get(k, _ENTRY_SENTINEL)
100+
if v is _ENTRY_SENTINEL:
101+
return None
102+
return MapEntry.of(k, v)
103+
104+
def val_at(self, k, default=None):
130105
return self._inner.get(k, default)
131106

132107
def update(self, *maps: Mapping[K, V]) -> "Map":

src/basilisp/lang/multifn.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ def __init__(
2626
def __call__(self, *args, **kwargs):
2727
key = self._dispatch(*args, **kwargs)
2828
method_cache = self.methods
29-
method: Optional[Method] = Maybe(method_cache.entry(key, None)).or_else(
30-
lambda: method_cache.entry(self._default, None) # type: ignore
29+
method: Optional[Method] = Maybe(method_cache.val_at(key, None)).or_else(
30+
lambda: method_cache.val_at(self._default, None) # type: ignore
3131
)
3232
if method:
3333
return method(*args, **kwargs)
@@ -49,8 +49,8 @@ def get_method(self, key: T) -> Optional[Method]:
4949
method_cache = self.methods
5050
# The 'type: ignore' comment below silences a spurious MyPy error
5151
# about having a return statement in a method which does not return.
52-
return Maybe(method_cache.entry(key, None)).or_else(
53-
lambda: method_cache.entry(self._default, None) # type: ignore
52+
return Maybe(method_cache.val_at(key, None)).or_else(
53+
lambda: method_cache.val_at(self._default, None) # type: ignore
5454
)
5555

5656
@staticmethod
@@ -60,7 +60,7 @@ def __remove_method(m: lmap.Map, key: T) -> lmap.Map:
6060

6161
def remove_method(self, key: T) -> Optional[Method]:
6262
"""Remove the method defined for this key and return it."""
63-
method = self.methods.entry(key, None)
63+
method = self.methods.val_at(key, None)
6464
if method:
6565
self._methods.swap(MultiFunction.__remove_method, key)
6666
return method

0 commit comments

Comments
 (0)