Skip to content

Commit db66a17

Browse files
authored
Protocols and Volatiles (#460)
Support Protocols and Volatiles fully. Protocols can be created via `(defprotocol Proto)` and are stored as regular maps in the Var designated by `Proto`. All type-making forms should support Protocols directly. Protocol support is buoyed by a few small compiler level changes. First, the compiler now statically computes the set of arities for a function (integers and the keyword `:rest` for the variadic method) and attaches them to the function via the `.arities` attribute. Secondly, the compiler attempts to statically check Protocol interface definitions by accessing the interface located at `(:interface proto)` (though can always fall back to the runtime check if required). All of the builtin collection types have had their MROs switched to place their corresponding collection interface highest in the method resolution order. This is critical for allowing the interface-matching capabilities of [`functools.singledispatch`](https://docs.python.org/3/library/functools.html#functools.singledispatch) to work in the most natural way for users in the event that a collection type matches multiple interfaces in the MRO. `basilisp.walk` was updated to use a protocol `IWalkable` both the validate that Protocols work and in the hopes that Protocol dispatch would be faster than the existing type which ran through a linear list of type checks to dispatch each nested invocation.
1 parent 940aba6 commit db66a17

File tree

14 files changed

+789
-67
lines changed

14 files changed

+789
-67
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
* Added metadata about the function or method context of a Lisp AST node in the `NodeEnv` (#548)
1414
* Added `reify*` special form (#425)
1515
* Added support for multi-arity methods on `definterface` (#538)
16+
* Added support for Protocols (#460)
17+
* Added support for Volatiles (#460)
1618

1719
### Fixed
1820
* Fixed a bug where the Basilisp AST nodes for return values of `deftype` members could be marked as _statements_ rather than _expressions_, resulting in an incorrect `nil` return (#523)

src/basilisp/core.lpy

Lines changed: 400 additions & 10 deletions
Large diffs are not rendered by default.

src/basilisp/lang/compiler/analyzer.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
SYM_PRIVATE_META_KEY,
6262
SYM_PROPERTY_META_KEY,
6363
SYM_STATICMETHOD_META_KEY,
64+
VAR_IS_PROTOCOL_META_KEY,
6465
SpecialForm,
6566
)
6667
from basilisp.lang.compiler.exception import CompilerException, CompilerPhase
@@ -150,6 +151,7 @@
150151
# Constants used in analyzing
151152
AS = kw.keyword("as")
152153
IMPLEMENTS = kw.keyword("implements")
154+
INTERFACE = kw.keyword("interface")
153155
STAR_STAR = sym.symbol("**")
154156
_DOUBLE_DOT_MACRO_NAME = ".."
155157
_BUILTINS_NS = "python"
@@ -1507,6 +1509,9 @@ def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-loca
15071509
return interfaces, members
15081510

15091511

1512+
_var_is_protocol = _meta_getter(VAR_IS_PROTOCOL_META_KEY)
1513+
1514+
15101515
def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-branches,too-many-locals
15111516
special_form: sym.Symbol,
15121517
fields: Iterable[str],
@@ -1543,7 +1548,15 @@ def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-bran
15431548
"and cannot be checked for abstractness; deferring to runtime",
15441549
)
15451550
return False
1546-
interface_type = interface.var.value
1551+
1552+
# Protocols are defined as maps, with the interface being simply a member
1553+
# of the map, denoted by the keyword `:interface`.
1554+
if _var_is_protocol(interface.var):
1555+
proto_map = interface.var.value
1556+
assert isinstance(proto_map, lmap.Map)
1557+
interface_type = proto_map.val_at(INTERFACE)
1558+
else:
1559+
interface_type = interface.var.value
15471560

15481561
if interface_type is object:
15491562
continue

src/basilisp/lang/compiler/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,5 @@ class SpecialForm:
5353
LINE_KW = kw.keyword("line")
5454
NAME_KW = kw.keyword("name")
5555
NS_KW = kw.keyword("ns")
56+
57+
VAR_IS_PROTOCOL_META_KEY = kw.keyword("protocol", "basilisp.core")

src/basilisp/lang/compiler/generator.py

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
SYM_DYNAMIC_META_KEY,
4242
SYM_NO_WARN_ON_REDEF_META_KEY,
4343
SYM_REDEF_META_KEY,
44+
VAR_IS_PROTOCOL_META_KEY,
4445
)
4546
from basilisp.lang.compiler.exception import CompilerException, CompilerPhase
4647
from basilisp.lang.compiler.nodes import (
@@ -1266,6 +1267,46 @@ def __deftype_member_to_py_ast(
12661267
return handle_deftype_member(ctx, node)
12671268

12681269

1270+
def __deftype_or_reify_bases_to_py_ast(
1271+
ctx: GeneratorContext, node: Union[DefType, Reify]
1272+
) -> List[ast.AST]:
1273+
"""Return a list of AST nodes for the base classes for a `deftype*` or `reify*`."""
1274+
assert node.op in {NodeOp.DEFTYPE, NodeOp.REIFY}
1275+
1276+
bases: List[ast.AST] = []
1277+
for base in node.interfaces:
1278+
base_node = gen_py_ast(ctx, base)
1279+
assert (
1280+
count(base_node.dependencies) == 0
1281+
), "Class and host form nodes do not have dependencies"
1282+
1283+
# Protocols are defined as Maps
1284+
if (
1285+
isinstance(base, VarRef)
1286+
and base.var.meta is not None
1287+
and base.var.meta.val_at(VAR_IS_PROTOCOL_META_KEY)
1288+
):
1289+
bases.append(
1290+
ast.Call(
1291+
func=ast.Attribute(
1292+
value=base_node.node, attr="val_at", ctx=ast.Load()
1293+
),
1294+
args=[
1295+
ast.Call(
1296+
func=_NEW_KW_FN_NAME,
1297+
args=[ast.Constant("interface")],
1298+
keywords=[],
1299+
)
1300+
],
1301+
keywords=[],
1302+
)
1303+
)
1304+
else:
1305+
bases.append(base_node.node)
1306+
1307+
return bases
1308+
1309+
12691310
@_with_ast_loc
12701311
def _deftype_to_py_ast( # pylint: disable=too-many-branches,too-many-locals
12711312
ctx: GeneratorContext, node: DefType
@@ -1275,13 +1316,7 @@ def _deftype_to_py_ast( # pylint: disable=too-many-branches,too-many-locals
12751316
type_name = munge(node.name)
12761317
ctx.symbol_table.new_symbol(sym.symbol(node.name), type_name, LocalType.DEFTYPE)
12771318

1278-
bases = []
1279-
for base in node.interfaces:
1280-
base_node = gen_py_ast(ctx, base)
1281-
assert (
1282-
count(base_node.dependencies) == 0
1283-
), "Class and host form nodes do not have dependencies"
1284-
bases.append(base_node.node)
1319+
bases = __deftype_or_reify_bases_to_py_ast(ctx, node)
12851320

12861321
with ctx.new_symbol_table(node.name):
12871322
fields = []
@@ -1469,6 +1504,35 @@ def __fn_args_to_py_ast(
14691504
return fn_args, varg, fn_body_ast
14701505

14711506

1507+
def __fn_decorator(arities: Iterable[int], has_rest_arg: bool = False,) -> ast.Call:
1508+
return ast.Call(
1509+
func=_BASILISP_FN_FN_NAME,
1510+
args=[],
1511+
keywords=[
1512+
ast.keyword(
1513+
arg="arities",
1514+
value=ast.Tuple(
1515+
elts=list(
1516+
chain(
1517+
map(ast.Constant, arities),
1518+
[
1519+
ast.Call(
1520+
func=_NEW_KW_FN_NAME,
1521+
args=[ast.Constant("rest")],
1522+
keywords=[],
1523+
)
1524+
]
1525+
if has_rest_arg
1526+
else [],
1527+
)
1528+
),
1529+
ctx=ast.Load(),
1530+
),
1531+
)
1532+
],
1533+
)
1534+
1535+
14721536
def __fn_meta(
14731537
ctx: GeneratorContext, meta_node: Optional[MetaNode] = None
14741538
) -> Tuple[Iterable[ast.AST], Iterable[ast.AST]]:
@@ -1549,7 +1613,14 @@ def __single_arity_fn_to_py_ast(
15491613
chain(
15501614
__kwargs_support_decorator(node),
15511615
meta_decorators,
1552-
[_BASILISP_FN_FN_NAME],
1616+
[
1617+
__fn_decorator(
1618+
(len(fn_args),)
1619+
if not method.is_variadic
1620+
else (),
1621+
has_rest_arg=method.is_variadic,
1622+
)
1623+
],
15531624
[_TRAMPOLINE_FN_NAME]
15541625
if ctx.recur_point.has_recur
15551626
else [],
@@ -1714,7 +1785,17 @@ def fn(*args):
17141785
kw_defaults=[],
17151786
),
17161787
body=body,
1717-
decorator_list=list(chain(meta_decorators, [_BASILISP_FN_FN_NAME])),
1788+
decorator_list=list(
1789+
chain(
1790+
meta_decorators,
1791+
[
1792+
__fn_decorator(
1793+
arity_map.keys(),
1794+
has_rest_arg=default_name is not None,
1795+
)
1796+
],
1797+
)
1798+
),
17181799
returns=None,
17191800
)
17201801
],
@@ -2291,14 +2372,10 @@ def _reify_to_py_ast(
22912372
else:
22922373
meta_ast = None
22932374

2294-
bases: List[ast.AST] = [_BASILISP_WITH_META_INTERFACE_NAME]
2295-
for base in node.interfaces:
2296-
base_node = gen_py_ast(ctx, base)
2297-
assert (
2298-
count(base_node.dependencies) == 0
2299-
), "Class and host form nodes do not have dependencies"
2300-
bases.append(base_node.node)
2301-
2375+
bases: List[ast.AST] = [
2376+
_BASILISP_WITH_META_INTERFACE_NAME,
2377+
*__deftype_or_reify_bases_to_py_ast(ctx, node),
2378+
]
23022379
type_name = munge(genname("ReifiedType"))
23032380

23042381
with ctx.new_symbol_table("reify"):

src/basilisp/lang/list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
T = TypeVar("T")
1111

1212

13-
class List(IWithMeta, ISeq[T], IPersistentList[T]):
13+
class List(IPersistentList[T], ISeq[T], IWithMeta):
1414
"""Basilisp List. Delegates internally to a pyrsistent.PList object.
1515
1616
Do not instantiate directly. Instead use the l() and list() factory

src/basilisp/lang/map.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
_ENTRY_SENTINEL = object()
2525

2626

27-
class Map(ILispObject, IWithMeta, IPersistentMap[K, V]):
27+
class Map(IPersistentMap[K, V], ILispObject, IWithMeta):
2828
"""Basilisp Map. Delegates internally to a pyrsistent.PMap object.
2929
Do not instantiate directly. Instead use the m() and map() factory
3030
methods below."""

src/basilisp/lang/runtime.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ class Namespace(ReferenceBase):
418418
[
419419
"attr",
420420
"builtins",
421+
"functools",
421422
"io",
422423
"importlib",
423424
"operator",
@@ -1149,7 +1150,7 @@ def contains(coll, k):
11491150
def get(m, k, default=None):
11501151
"""Return the value of k in m. Return default if k not found in m."""
11511152
if isinstance(m, ILookup):
1152-
return m.val_at(k, default=default)
1153+
return m.val_at(k, default)
11531154

11541155
try:
11551156
return m[k]
@@ -1453,14 +1454,19 @@ def wrapped_f(*args, **kwargs):
14531454
return wrapped_f
14541455

14551456

1456-
def _basilisp_fn(f):
1457+
def _basilisp_fn(arities: Tuple[Union[int, kw.Keyword]]):
14571458
"""Create a Basilisp function, setting meta and supplying a with_meta
14581459
method implementation."""
1459-
assert not hasattr(f, "meta")
1460-
f._basilisp_fn = True
1461-
f.meta = None
1462-
f.with_meta = partial(_fn_with_meta, f)
1463-
return f
1460+
1461+
def wrap_fn(f):
1462+
assert not hasattr(f, "meta")
1463+
f._basilisp_fn = True
1464+
f.arities = lset.set(arities)
1465+
f.meta = None
1466+
f.with_meta = partial(_fn_with_meta, f)
1467+
return f
1468+
1469+
return wrap_fn
14641470

14651471

14661472
def _basilisp_type(

src/basilisp/lang/set.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
T = TypeVar("T")
1616

1717

18-
class Set(IWithMeta, ILispObject, IPersistentSet[T]):
18+
class Set(IPersistentSet[T], ILispObject, IWithMeta):
1919
"""Basilisp Set. Delegates internally to a pyrsistent.PSet object.
2020
2121
Do not instantiate directly. Instead use the s() and set() factory

src/basilisp/lang/vector.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
T = TypeVar("T")
1717

1818

19-
class Vector(ILispObject, IWithMeta, IPersistentVector[T]):
19+
class Vector(IPersistentVector[T], ILispObject, IWithMeta):
2020
"""Basilisp Vector. Delegates internally to a pyrsistent.PVector object.
2121
Do not instantiate directly. Instead use the v() and vec() factory
2222
methods below."""

0 commit comments

Comments
 (0)