Skip to content

Commit c6eeea9

Browse files
authored
Validate that dynamic interfaces are abstract for deftypes (#546)
* Validate that dynamic interfaces are abstract for deftypes * Please our linting overlords * Extra required cases
1 parent 5833597 commit c6eeea9

File tree

8 files changed

+513
-197
lines changed

8 files changed

+513
-197
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
* Fixed a bug where Basilisp would throw an exception when comparing seqs by `=` to non-seqable values (#530)
1818
* Fixed a bug where aliased Python submodule imports referred to the top-level module rather than the submodule (#533)
1919
* Fixed a bug where static methods and class methods on types created by `deftype` could not be referred to directly (defeating the purpose of the static or class method) (#537)
20-
* Fixed a bug where `defftype` forms could not be declared without at least one field (#540)
20+
* Fixed a bug where `deftype` forms could not be declared without at least one field (#540)
2121
* Fixed a bug where not all builtin Basilisp types could be pickled (#518)
22+
* Fixed a bug where `deftype` forms could not be created interfaces declared not at the top-level of a code block in a namespace (#376)
2223

2324
## [v0.1.dev13] - 2020-03-16
2425
### Added

src/basilisp/lang/compiler/analyzer.py

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import builtins
22
import collections
33
import contextlib
4-
import inspect
54
import logging
65
import re
76
import sys
@@ -50,7 +49,6 @@
5049
LINE_KW,
5150
NAME_KW,
5251
NS_KW,
53-
OBJECT_DUNDER_METHODS,
5452
SYM_ASYNC_META_KEY,
5553
SYM_CLASSMETHOD_META_KEY,
5654
SYM_DEFAULT_META_KEY,
@@ -129,7 +127,8 @@
129127
from basilisp.lang.interfaces import IMeta, IRecord, ISeq, IType, IWithMeta
130128
from basilisp.lang.runtime import Var
131129
from basilisp.lang.typing import CompilerOpts, LispForm, ReaderForm
132-
from basilisp.lang.util import count, genname, munge
130+
from basilisp.lang.util import OBJECT_DUNDER_METHODS, count, genname, is_abstract, munge
131+
from basilisp.logconfig import TRACE
133132
from basilisp.util import Maybe, partition
134133

135134
# Analyzer logging
@@ -1467,41 +1466,45 @@ def __deftype_impls( # pylint: disable=too-many-branches,too-many-locals # noq
14671466
return interfaces, members
14681467

14691468

1470-
def __is_abstract(tp: Type) -> bool:
1471-
"""Return True if tp is an abstract class.
1472-
1473-
The builtin inspect.isabstract returns False for marker abstract classes
1474-
which do not define any abstract members."""
1475-
if inspect.isabstract(tp):
1476-
return True
1477-
return (
1478-
inspect.isclass(tp)
1479-
and hasattr(tp, "__abstractmethods__")
1480-
and tp.__abstractmethods__ == frozenset()
1481-
)
1482-
1483-
1484-
def __assert_deftype_impls_are_abstract( # pylint: disable=too-many-branches,too-many-locals
1469+
def __deftype_impls_are_all_abstract( # pylint: disable=too-many-branches,too-many-locals
14851470
fields: Iterable[str],
14861471
interfaces: Iterable[DefTypeBase],
14871472
members: Iterable[DefTypeMember],
1488-
) -> None:
1473+
) -> bool:
1474+
"""Return True if all `deftype*` super-types can be verified abstract statically.
1475+
Return False otherwise.
1476+
1477+
In certain cases, such as in macro definitions and potentially inside of functions,
1478+
the compiler will be unable to resolve the named super-type as an object during
1479+
compilation and these checks will need to be deferred to runtime. In these cases,
1480+
the compiler will wrap the emitted class in a decorator that performs the checks
1481+
when the class is compiled by the Python compiler.
1482+
1483+
For normal compile-time errors, an `AnalyzerException` will be raised."""
14891484
field_names = frozenset(fields)
14901485
member_names = frozenset(munge(member.name) for member in members)
14911486
all_member_names = field_names.union(member_names)
14921487
all_interface_methods: Set[str] = set()
14931488
for interface in interfaces:
14941489
if isinstance(interface, (MaybeClass, MaybeHostForm)):
14951490
interface_type = interface.target
1496-
elif isinstance(interface, VarRef):
1491+
else:
1492+
assert isinstance(
1493+
interface, VarRef
1494+
), "Interface must be MaybeClass, MaybeHostForm, or VarRef"
1495+
if not interface.var.is_bound:
1496+
logger.log(
1497+
TRACE,
1498+
f"deftype* interface Var '{interface.form}' is not bound and "
1499+
"cannot be checked for abstractness; deferring to runtime",
1500+
)
1501+
return False
14971502
interface_type = interface.var.value
1498-
else: # pragma: no cover
1499-
assert False, "Interface must be MaybeClass, MaybeHostForm, or VarRef"
15001503

15011504
if interface_type is object:
15021505
continue
15031506

1504-
if not __is_abstract(interface_type):
1507+
if not is_abstract(interface_type):
15051508
raise AnalyzerException(
15061509
"deftype* interface must be Python abstract class or object",
15071510
form=interface.form,
@@ -1542,6 +1545,8 @@ def __assert_deftype_impls_are_abstract( # pylint: disable=too-many-branches,to
15421545
f"defined interfaces: {extra_method_str}"
15431546
)
15441547

1548+
return True
1549+
15451550

15461551
__DEFTYPE_DEFAULT_SENTINEL = object()
15471552

@@ -1620,7 +1625,7 @@ def _deftype_ast( # pylint: disable=too-many-branches
16201625
ctx.put_new_symbol(field, binding, warn_if_unused=False)
16211626

16221627
interfaces, members = __deftype_impls(ctx, runtime.nthrest(form, 3))
1623-
__assert_deftype_impls_are_abstract(
1628+
verified_abstract = __deftype_impls_are_all_abstract(
16241629
map(lambda f: f.name, fields), interfaces, members
16251630
)
16261631
return DefType(
@@ -1629,6 +1634,7 @@ def _deftype_ast( # pylint: disable=too-many-branches
16291634
interfaces=vec.vector(interfaces),
16301635
fields=vec.vector(param_nodes),
16311636
members=vec.vector(members),
1637+
verified_abstract=verified_abstract,
16321638
is_frozen=is_frozen,
16331639
env=ctx.get_node_env(pos=ctx.syntax_position),
16341640
)

src/basilisp/lang/compiler/constants.py

Lines changed: 0 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -30,111 +30,6 @@ class SpecialForm:
3030

3131
DEFAULT_COMPILER_FILE_PATH = "NO_SOURCE_PATH"
3232

33-
# Trimmed list of __dunder__ methods generated by using this command:
34-
#
35-
# find "$(pipenv --venv)/lib/python3.6" \
36-
# -name '*.py' \
37-
# -exec egrep \
38-
# -oh '__[A-Za-z_][A-Za-z_0-9]*__' '{}' \; \
39-
# | sort | uniq
40-
OBJECT_DUNDER_METHODS = frozenset(
41-
{
42-
"__abs__",
43-
"__add__",
44-
"__aenter__",
45-
"__aexit__",
46-
"__aiter__",
47-
"__await__",
48-
"__bytes__",
49-
"__call__",
50-
"__complex__",
51-
"__contains__",
52-
"__del__",
53-
"__delattr__",
54-
"__delitem__",
55-
"__delslice__",
56-
"__dict__",
57-
"__dir__",
58-
"__div__",
59-
"__divmod__",
60-
"__enter__",
61-
"__eq__",
62-
"__exit__",
63-
"__float__",
64-
"__floordiv__",
65-
"__ge__",
66-
"__getattr__",
67-
"__getattribute__",
68-
"__getitem__",
69-
"__getslice__",
70-
"__getstate__",
71-
"__gt__",
72-
"__hash__",
73-
"__iadd__",
74-
"__iand__",
75-
"__idiv__",
76-
"__ifloordiv__",
77-
"__ilshift__",
78-
"__imatmul__",
79-
"__imod__",
80-
"__imul__",
81-
"__init__",
82-
"__instancecheck__",
83-
"__int__",
84-
"__invert__",
85-
"__ior__",
86-
"__ipow__",
87-
"__isub__",
88-
"__iter__",
89-
"__itruediv__",
90-
"__ixor__",
91-
"__le__",
92-
"__len__",
93-
"__lshift__",
94-
"__matmul__",
95-
"__mod__",
96-
"__mul__",
97-
"__ne__",
98-
"__neg__",
99-
"__new__",
100-
"__not__",
101-
"__pos__",
102-
"__pow__",
103-
"__radd__",
104-
"__rand__",
105-
"__rcmp__",
106-
"__rdiv__",
107-
"__rdivmod__",
108-
"__reduce__",
109-
"__reduce_ex__",
110-
"__repr__",
111-
"__rfloordiv__",
112-
"__rlshift__",
113-
"__rmatmul__",
114-
"__rmod__",
115-
"__rmul__",
116-
"__rne__",
117-
"__ror__",
118-
"__round__",
119-
"__rpow__",
120-
"__rrshift__",
121-
"__rshift__",
122-
"__rsub__",
123-
"__rtruediv__",
124-
"__rxor__",
125-
"__setattr__",
126-
"__setitem__",
127-
"__setslice__",
128-
"__setstate__",
129-
"__str__",
130-
"__sub__",
131-
"__subclasscheck__",
132-
"__subclasshook__",
133-
"__truediv__",
134-
"__xor__",
135-
}
136-
)
137-
13833
SYM_ASYNC_META_KEY = kw.keyword("async")
13934
SYM_KWARGS_META_KEY = kw.keyword("kwargs")
14035
SYM_PRIVATE_META_KEY = kw.keyword("private")

src/basilisp/lang/compiler/generator.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ def _var_ns_as_python_sym(name: str) -> str:
554554
_COERCE_SEQ_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}.to_seq")
555555
_BASILISP_FN_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}._basilisp_fn")
556556
_FN_WITH_ATTRS_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}._with_attrs")
557+
_BASILISP_TYPE_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}._basilisp_type")
557558
_BUILTINS_IMPORT_FN_NAME = _load_attr("builtins.__import__")
558559
_IMPORTLIB_IMPORT_MODULE_FN_NAME = _load_attr("importlib.import_module")
559560
_LISP_FN_APPLY_KWARGS_FN_NAME = _load_attr(f"{_RUNTIME_ALIAS}._lisp_fn_apply_kwargs")
@@ -1268,7 +1269,7 @@ def __deftype_member_to_py_ast(
12681269

12691270

12701271
@_with_ast_loc
1271-
def _deftype_to_py_ast( # pylint: disable=too-many-branches
1272+
def _deftype_to_py_ast( # pylint: disable=too-many-branches,too-many-locals
12721273
ctx: GeneratorContext, node: DefType
12731274
) -> GeneratedPyAST:
12741275
"""Return a Python AST Node for a `deftype*` expression."""
@@ -1284,17 +1285,8 @@ def _deftype_to_py_ast( # pylint: disable=too-many-branches
12841285
), "Class and host form nodes do not have dependencies"
12851286
bases.append(base_node.node)
12861287

1287-
decorator = ast.Call(
1288-
func=_ATTR_CLASS_DECORATOR_NAME,
1289-
args=[],
1290-
keywords=_ATTR_CMP_KWARGS
1291-
+ [
1292-
ast.keyword(arg="frozen", value=ast.Constant(node.is_frozen)),
1293-
ast.keyword(arg="slots", value=ast.Constant(True)),
1294-
],
1295-
)
1296-
12971288
with ctx.new_symbol_table(node.name):
1289+
fields, members = [], []
12981290
type_nodes: List[ast.AST] = []
12991291
type_deps: List[ast.AST] = []
13001292
for field in node.fields:
@@ -1318,11 +1310,13 @@ def _deftype_to_py_ast( # pylint: disable=too-many-branches
13181310
)
13191311
)
13201312
ctx.symbol_table.new_symbol(sym.symbol(field.name), safe_field, field.local)
1313+
fields.append(safe_field)
13211314

13221315
for member in node.members:
13231316
type_ast = __deftype_member_to_py_ast(ctx, member, node)
13241317
type_nodes.append(type_ast.node)
13251318
type_nodes.extend(type_ast.dependencies)
1319+
members.append(munge(member.name))
13261320

13271321
return GeneratedPyAST(
13281322
node=ast.Name(id=type_name, ctx=ast.Load()),
@@ -1335,7 +1329,64 @@ def _deftype_to_py_ast( # pylint: disable=too-many-branches
13351329
bases=bases,
13361330
keywords=[],
13371331
body=type_nodes or [ast.Pass()],
1338-
decorator_list=[decorator],
1332+
decorator_list=list(
1333+
chain(
1334+
[]
1335+
if node.verified_abstract
1336+
else [
1337+
ast.Call(
1338+
func=_BASILISP_TYPE_FN_NAME,
1339+
args=[],
1340+
keywords=[
1341+
ast.keyword(
1342+
arg="fields",
1343+
value=ast.Tuple(
1344+
elts=[
1345+
ast.Constant(e)
1346+
for e in fields
1347+
],
1348+
ctx=ast.Load(),
1349+
),
1350+
),
1351+
ast.keyword(
1352+
arg="interfaces",
1353+
value=ast.Tuple(
1354+
elts=list(bases),
1355+
ctx=ast.Load(),
1356+
),
1357+
),
1358+
ast.keyword(
1359+
arg="members",
1360+
value=ast.Tuple(
1361+
elts=[
1362+
ast.Constant(e)
1363+
for e in members
1364+
],
1365+
ctx=ast.Load(),
1366+
),
1367+
),
1368+
],
1369+
)
1370+
],
1371+
[
1372+
ast.Call(
1373+
func=_ATTR_CLASS_DECORATOR_NAME,
1374+
args=[],
1375+
keywords=_ATTR_CMP_KWARGS
1376+
+ [
1377+
ast.keyword(
1378+
arg="frozen",
1379+
value=ast.Constant(node.is_frozen),
1380+
),
1381+
ast.keyword(
1382+
arg="slots",
1383+
value=ast.Constant(True),
1384+
),
1385+
],
1386+
),
1387+
],
1388+
)
1389+
),
13391390
),
13401391
ast.Call(
13411392
func=_INTERN_VAR_FN_NAME,

src/basilisp/lang/compiler/nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ class DefType(Node[SpecialForm]):
380380
fields: Iterable[Binding]
381381
members: Iterable["DefTypeMember"]
382382
env: NodeEnv
383+
verified_abstract: bool = False
383384
is_frozen: bool = True
384385
meta: NodeMeta = None
385386
children: Sequence[kw.Keyword] = vec.v(FIELDS, MEMBERS)

0 commit comments

Comments
 (0)