Skip to content

Commit 955d0bf

Browse files
authored
Support artificially abstract base classes (#565)
Add support for "artificially abstract" base classes. The Python ecosystem is much less strict with its use of `abc.ABC` to interfaces than Java (which has a native `interface` construct), so even in where a type may be _in practice_ an interface or ABC, the compiler would not permit you to declare such types as supertypes because they do not inherit from `abc.ABC`. In these cases, users can mark the type as abstract with the `:abstract` metadata key to force the compiler to accept that it is abstract. In this case, the compiler will attempt to verify that any extra methods present on the `deftype`, `defrecord`, or `reify` are present on any one of the artificially abstract base classes. This will allow users to override existing class members, but not introduce new ones without a true interface.
1 parent 5da4251 commit 955d0bf

File tree

10 files changed

+363
-95
lines changed

10 files changed

+363
-95
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
* Added support for multi-arity methods on `definterface` (#538)
1616
* Added support for Protocols (#460)
1717
* Added support for Volatiles (#460)
18-
* Add JSON encoder and decoder in `basilisp.json` namespace (#484)
18+
* Added JSON encoder and decoder in `basilisp.json` namespace (#484)
19+
* Added support for generically diffing Basilisp data structures in `basilisp.data` namespace (#555)
20+
* Added support for artificially abstract bases classes in `deftype`, `defrecord`, and `reify` types (#565)
1921

2022
### Changed
2123
* Basilisp set and map types are now backed by the HAMT provided by `immutables` (#557)

src/basilisp/lang/compiler/analyzer.py

Lines changed: 105 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import builtins
22
import collections
33
import contextlib
4+
import inspect
45
import logging
56
import re
67
import sys
@@ -49,6 +50,7 @@
4950
LINE_KW,
5051
NAME_KW,
5152
NS_KW,
53+
SYM_ABSTRACT_META_KEY,
5254
SYM_ASYNC_META_KEY,
5355
SYM_CLASSMETHOD_META_KEY,
5456
SYM_DEFAULT_META_KEY,
@@ -623,6 +625,7 @@ def has_meta_prop(o: Union[IMeta, Var]) -> bool:
623625
return has_meta_prop
624626

625627

628+
_is_artificially_abstract = _meta_getter(SYM_ABSTRACT_META_KEY)
626629
_is_async = _meta_getter(SYM_ASYNC_META_KEY)
627630
_is_mutable = _meta_getter(SYM_MUTABLE_META_KEY)
628631
_is_py_classmethod = _meta_getter(SYM_CLASSMETHOD_META_KEY)
@@ -1512,24 +1515,57 @@ def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-loca
15121515
_var_is_protocol = _meta_getter(VAR_IS_PROTOCOL_META_KEY)
15131516

15141517

1518+
def __is_deftype_member(mem) -> bool:
1519+
"""Return True if `mem` names a valid `deftype*` member."""
1520+
return (
1521+
inspect.isfunction(mem)
1522+
or isinstance(mem, (property, staticmethod))
1523+
or inspect.ismethod(mem)
1524+
)
1525+
1526+
1527+
def __is_reify_member(mem) -> bool:
1528+
"""Return True if `mem` names a valid `reify*` member."""
1529+
return inspect.isfunction(mem) or isinstance(mem, property)
1530+
1531+
15151532
def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-branches,too-many-locals
15161533
special_form: sym.Symbol,
15171534
fields: Iterable[str],
15181535
interfaces: Iterable[DefTypeBase],
15191536
members: Iterable[DefTypeMember],
1520-
) -> bool:
1521-
"""Return True if all `deftype*` or `reify*` super-types can be verified abstract
1522-
statically. Return False otherwise.
1537+
) -> Tuple[bool, lset.Set[DefTypeBase]]:
1538+
"""Return a tuple of two items indicating the abstractness of the `deftype*` or
1539+
`reify*` super-types. The first element is a boolean value which, if True,
1540+
indicates that all bases have been statically verified abstract. If False, that
1541+
value indicates at least one base could not be statically verified. The second
1542+
element is the set of all super-types which have been marked as artificially
1543+
abstract.
15231544
15241545
In certain cases, such as in macro definitions and potentially inside of
15251546
functions, the compiler will be unable to resolve the named super-type as an
15261547
object during compilation and these checks will need to be deferred to runtime.
15271548
In these cases, the compiler will wrap the emitted class in a decorator that
15281549
performs the checks when the class is compiled by the Python compiler.
15291550
1551+
The Python ecosystem is much less strict with its use of `abc.ABC` to define
1552+
interfaces than Java (which has a native `interface` construct), so even in cases
1553+
where a type may be _in practice_ an interface or ABC, the compiler would not
1554+
permit you to declare such types as supertypes because they do not themselves
1555+
inherit from `abc.ABC`. In these cases, users can mark the type as artificially
1556+
abstract with the `:abstract` metadata key.
1557+
15301558
For normal compile-time errors, an `AnalyzerException` will be raised."""
15311559
assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
15321560

1561+
unverifiably_abstract = set()
1562+
artificially_abstract: Set[DefTypeBase] = set()
1563+
artificially_abstract_base_members: Set[str] = set()
1564+
is_member = {
1565+
SpecialForm.DEFTYPE: __is_deftype_member,
1566+
SpecialForm.REIFY: __is_reify_member,
1567+
}[special_form]
1568+
15331569
field_names = frozenset(fields)
15341570
member_names = frozenset(deftype_or_reify_python_member_names(members))
15351571
all_member_names = field_names.union(member_names)
@@ -1547,7 +1583,10 @@ def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-bran
15471583
f"{special_form} interface Var '{interface.form}' is not bound"
15481584
"and cannot be checked for abstractness; deferring to runtime",
15491585
)
1550-
return False
1586+
unverifiably_abstract.add(interface)
1587+
if _is_artificially_abstract(interface.form):
1588+
artificially_abstract.add(interface)
1589+
continue
15511590

15521591
# Protocols are defined as maps, with the interface being simply a member
15531592
# of the map, denoted by the keyword `:interface`.
@@ -1561,54 +1600,72 @@ def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-bran
15611600
if interface_type is object:
15621601
continue
15631602

1564-
if not is_abstract(interface_type):
1565-
raise AnalyzerException(
1566-
f"{special_form} interface must be Python abstract class or object",
1567-
form=interface.form,
1568-
lisp_ast=interface,
1603+
if is_abstract(interface_type):
1604+
interface_names: FrozenSet[str] = interface_type.__abstractmethods__
1605+
interface_property_names: FrozenSet[str] = frozenset(
1606+
method
1607+
for method in interface_names
1608+
if isinstance(getattr(interface_type, method), property)
15691609
)
1610+
interface_method_names = interface_names - interface_property_names
1611+
if not interface_method_names.issubset(member_names):
1612+
missing_methods = ", ".join(interface_method_names - member_names)
1613+
raise AnalyzerException(
1614+
f"{special_form} definition missing interface members for "
1615+
f"interface {interface.form}: {missing_methods}",
1616+
form=interface.form,
1617+
lisp_ast=interface,
1618+
)
1619+
elif not interface_property_names.issubset(all_member_names):
1620+
missing_fields = ", ".join(interface_property_names - field_names)
1621+
raise AnalyzerException(
1622+
f"{special_form} definition missing interface properties for "
1623+
f"interface {interface.form}: {missing_fields}",
1624+
form=interface.form,
1625+
lisp_ast=interface,
1626+
)
15701627

1571-
interface_names: FrozenSet[str] = interface_type.__abstractmethods__
1572-
interface_property_names: FrozenSet[str] = frozenset(
1573-
method
1574-
for method in interface_names
1575-
if isinstance(getattr(interface_type, method), property)
1576-
)
1577-
interface_method_names = interface_names - interface_property_names
1578-
if not interface_method_names.issubset(member_names):
1579-
missing_methods = ", ".join(interface_method_names - member_names)
1580-
raise AnalyzerException(
1581-
f"{special_form} definition missing interface members for "
1582-
f"interface {interface.form}: {missing_methods}",
1583-
form=interface.form,
1584-
lisp_ast=interface,
1628+
all_interface_methods.update(interface_names)
1629+
elif _is_artificially_abstract(interface.form):
1630+
# Given that artificially abstract bases aren't real `abc.ABC`s and do
1631+
# not annotate their `abstractmethod`s, we can't assert right now that
1632+
# any the type will satisfy the artificially abstract base. However,
1633+
# we can collect any defined methods into a set for artificial bases
1634+
# and assert that any extra methods are included in that set below.
1635+
artificially_abstract.add(interface)
1636+
artificially_abstract_base_members.update(
1637+
map(
1638+
lambda v: v[0],
1639+
inspect.getmembers(interface_type, predicate=is_member),
1640+
)
15851641
)
1586-
elif not interface_property_names.issubset(all_member_names):
1587-
missing_fields = ", ".join(interface_property_names - field_names)
1642+
else:
15881643
raise AnalyzerException(
1589-
f"{special_form} definition missing interface properties for "
1590-
f"interface {interface.form}: {missing_fields}",
1644+
f"{special_form} interface must be Python abstract class or object",
15911645
form=interface.form,
15921646
lisp_ast=interface,
15931647
)
15941648

1595-
all_interface_methods.update(interface_names)
1596-
1597-
extra_methods = member_names - all_interface_methods - OBJECT_DUNDER_METHODS
1598-
if extra_methods:
1599-
extra_method_str = ", ".join(extra_methods)
1600-
raise AnalyzerException(
1601-
f"{special_form} definition for interface includes members not "
1602-
f"part of defined interfaces: {extra_method_str}"
1603-
)
1649+
# We cannot compute if there are extra methods defined if there are any
1650+
# unverifiably abstract bases, so we just skip this check.
1651+
if not unverifiably_abstract:
1652+
extra_methods = member_names - all_interface_methods - OBJECT_DUNDER_METHODS
1653+
if extra_methods and not extra_methods.issubset(
1654+
artificially_abstract_base_members
1655+
):
1656+
extra_method_str = ", ".join(extra_methods)
1657+
raise AnalyzerException(
1658+
f"{special_form} definition for interface includes members not "
1659+
f"part of defined interfaces: {extra_method_str}"
1660+
)
16041661

1605-
return True
1662+
return not unverifiably_abstract, lset.set(artificially_abstract)
16061663

16071664

16081665
__DEFTYPE_DEFAULT_SENTINEL = object()
16091666

16101667

1611-
def _deftype_ast( # pylint: disable=too-many-branches
1668+
def _deftype_ast( # pylint: disable=too-many-branches,too-many-locals
16121669
ctx: AnalyzerContext, form: ISeq
16131670
) -> DefType:
16141671
assert form.first == SpecialForm.DEFTYPE
@@ -1684,7 +1741,10 @@ def _deftype_ast( # pylint: disable=too-many-branches
16841741
interfaces, members = __deftype_or_reify_impls(
16851742
ctx, runtime.nthrest(form, 3), SpecialForm.DEFTYPE
16861743
)
1687-
verified_abstract = __deftype_and_reify_impls_are_all_abstract(
1744+
(
1745+
verified_abstract,
1746+
artificially_abstract,
1747+
) = __deftype_and_reify_impls_are_all_abstract(
16881748
SpecialForm.DEFTYPE, map(lambda f: f.name, fields), interfaces, members
16891749
)
16901750
return DefType(
@@ -1694,6 +1754,7 @@ def _deftype_ast( # pylint: disable=too-many-branches
16941754
fields=vec.vector(param_nodes),
16951755
members=vec.vector(members),
16961756
verified_abstract=verified_abstract,
1757+
artificially_abstract=artificially_abstract,
16971758
is_frozen=is_frozen,
16981759
env=ctx.get_node_env(pos=ctx.syntax_position),
16991760
)
@@ -2552,14 +2613,18 @@ def _reify_ast(ctx: AnalyzerContext, form: ISeq) -> Reify:
25522613
interfaces, members = __deftype_or_reify_impls(
25532614
ctx, runtime.nthrest(form, 1), SpecialForm.REIFY
25542615
)
2555-
verified_abstract = __deftype_and_reify_impls_are_all_abstract(
2616+
(
2617+
verified_abstract,
2618+
artificially_abstract,
2619+
) = __deftype_and_reify_impls_are_all_abstract(
25562620
SpecialForm.REIFY, (), interfaces, members
25572621
)
25582622
return Reify(
25592623
form=form,
25602624
interfaces=vec.vector(interfaces),
25612625
members=vec.vector(members),
25622626
verified_abstract=verified_abstract,
2627+
artificially_abstract=artificially_abstract,
25632628
env=ctx.get_node_env(pos=ctx.syntax_position),
25642629
)
25652630

src/basilisp/lang/compiler/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class SpecialForm:
3131

3232
DEFAULT_COMPILER_FILE_PATH = "NO_SOURCE_PATH"
3333

34+
SYM_ABSTRACT_META_KEY = kw.keyword("abstract")
3435
SYM_ASYNC_META_KEY = kw.keyword("async")
3536
SYM_KWARGS_META_KEY = kw.keyword("kwargs")
3637
SYM_PRIVATE_META_KEY = kw.keyword("private")

src/basilisp/lang/compiler/generator.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
ConstType,
5353
Def,
5454
DefType,
55+
DefTypeBase,
5556
DefTypeClassMethod,
5657
DefTypeMember,
5758
DefTypeMethod,
@@ -397,6 +398,7 @@ def _class_ast( # pylint: disable=too-many-arguments
397398
fields: Iterable[str] = (),
398399
members: Iterable[str] = (),
399400
verified_abstract: bool = False,
401+
artificially_abstract_bases: Iterable[ast.AST] = (),
400402
is_frozen: bool = True,
401403
use_slots: bool = _ATTR_SLOTS_ON,
402404
) -> ast.ClassDef:
@@ -426,6 +428,10 @@ def _class_ast( # pylint: disable=too-many-arguments
426428
arg="interfaces",
427429
value=ast.Tuple(elts=list(bases), ctx=ast.Load()),
428430
),
431+
ast.keyword(
432+
arg="artificially_abstract_bases",
433+
value=ast.Set(elts=list(artificially_abstract_bases)),
434+
),
429435
ast.keyword(
430436
arg="members",
431437
value=ast.Tuple(
@@ -1268,13 +1274,15 @@ def __deftype_member_to_py_ast(
12681274

12691275

12701276
def __deftype_or_reify_bases_to_py_ast(
1271-
ctx: GeneratorContext, node: Union[DefType, Reify]
1277+
ctx: GeneratorContext,
1278+
node: Union[DefType, Reify],
1279+
interfaces: Iterable[DefTypeBase],
12721280
) -> List[ast.AST]:
12731281
"""Return a list of AST nodes for the base classes for a `deftype*` or `reify*`."""
12741282
assert node.op in {NodeOp.DEFTYPE, NodeOp.REIFY}
12751283

12761284
bases: List[ast.AST] = []
1277-
for base in node.interfaces:
1285+
for base in interfaces:
12781286
base_node = gen_py_ast(ctx, base)
12791287
assert (
12801288
count(base_node.dependencies) == 0
@@ -1316,7 +1324,10 @@ def _deftype_to_py_ast( # pylint: disable=too-many-branches,too-many-locals
13161324
type_name = munge(node.name)
13171325
ctx.symbol_table.new_symbol(sym.symbol(node.name), type_name, LocalType.DEFTYPE)
13181326

1319-
bases = __deftype_or_reify_bases_to_py_ast(ctx, node)
1327+
bases = __deftype_or_reify_bases_to_py_ast(ctx, node, node.interfaces)
1328+
artificially_abstract_bases = __deftype_or_reify_bases_to_py_ast(
1329+
ctx, node, node.artificially_abstract
1330+
)
13201331

13211332
with ctx.new_symbol_table(node.name):
13221333
fields = []
@@ -1363,6 +1374,7 @@ def _deftype_to_py_ast( # pylint: disable=too-many-branches,too-many-locals
13631374
fields=fields,
13641375
members=node.python_member_names,
13651376
verified_abstract=node.verified_abstract,
1377+
artificially_abstract_bases=artificially_abstract_bases,
13661378
is_frozen=node.is_frozen,
13671379
use_slots=True,
13681380
),
@@ -2374,8 +2386,11 @@ def _reify_to_py_ast(
23742386

23752387
bases: List[ast.AST] = [
23762388
_BASILISP_WITH_META_INTERFACE_NAME,
2377-
*__deftype_or_reify_bases_to_py_ast(ctx, node),
2389+
*__deftype_or_reify_bases_to_py_ast(ctx, node, node.interfaces),
23782390
]
2391+
artificially_abstract_bases = __deftype_or_reify_bases_to_py_ast(
2392+
ctx, node, node.artificially_abstract
2393+
)
23792394
type_name = munge(genname("ReifiedType"))
23802395

23812396
with ctx.new_symbol_table("reify"):
@@ -2454,6 +2469,7 @@ def _reify_to_py_ast(
24542469
["meta", "with_meta"], node.python_member_names
24552470
),
24562471
verified_abstract=node.verified_abstract,
2472+
artificially_abstract_bases=artificially_abstract_bases,
24572473
is_frozen=True,
24582474
use_slots=_ATTR_SLOTS_ON,
24592475
)

src/basilisp/lang/compiler/nodes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import basilisp.lang.set as lset
2121
import basilisp.lang.symbol as sym
2222
import basilisp.lang.vector as vec
23-
from basilisp.lang.interfaces import IPersistentMap, IPersistentVector
23+
from basilisp.lang.interfaces import IPersistentMap, IPersistentSet, IPersistentVector
2424
from basilisp.lang.runtime import Namespace, Var, to_lisp
2525
from basilisp.lang.typing import LispForm, ReaderForm as ReaderLispForm, SpecialForm
2626
from basilisp.lang.util import munge
@@ -405,6 +405,7 @@ class DefType(Node[SpecialForm]):
405405
members: Iterable["DefTypeMember"]
406406
env: NodeEnv
407407
verified_abstract: bool = False
408+
artificially_abstract: IPersistentSet[DefTypeBase] = lset.Set.empty()
408409
is_frozen: bool = True
409410
meta: NodeMeta = None
410411
children: Sequence[kw.Keyword] = vec.v(FIELDS, MEMBERS)
@@ -802,6 +803,7 @@ class Reify(Node[SpecialForm]):
802803
members: Iterable["DefTypeMember"]
803804
env: NodeEnv
804805
verified_abstract: bool = False
806+
artificially_abstract: IPersistentSet[DefTypeBase] = lset.Set.empty()
805807
meta: NodeMeta = None
806808
children: Sequence[kw.Keyword] = vec.v(MEMBERS)
807809
op: NodeOp = NodeOp.REIFY

0 commit comments

Comments
 (0)