Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added support for Lisp functions being called with keyword arguments (#528)
* Added support for multi-arity methods on `deftype`s (#534)
* Added metadata about the function or method context of a Lisp AST node in the `NodeEnv` (#548)
* Added `reify*` special form (#425)
* Added support for multi-arity methods on `definterface` (#538)

### Fixed
* 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)
Expand Down
98 changes: 70 additions & 28 deletions src/basilisp/core.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -4480,7 +4480,7 @@
Options may be specified as key-value pairs. The following options are supported:
- :name - the name of the interface as a string; required
- :extends - a vector of interfaces the new interface should extend; optional
- :methods - an optional vector of method signatures like:
- :methods - an optional vector of method signatures without `self` or `this`, like:
[ (method-name [args ...] docstring) ... ]

Callers should use `definterface` to generate new interfaces."
Expand All @@ -4490,28 +4490,73 @@
extends (as-> (:extends opt-map []) $
(remove #(identical? abc/ABC %) $)
(concat $ [abc/ABC])
(python/tuple $))

methods (reduce (fn [m [method-name args docstring]]
(let [method-args (->> (concat ['^:no-warn-when-unused self] args)
(apply vector))
method (->> (list 'fn* method-name method-args)
(eval)
(abc/abstractmethod))]
(when docstring
(set! (.- method __doc__) docstring))
(assoc m (munge method-name) method)))
{}
(:methods opt-map))]
(python/type interface-name
extends
(lisp->py methods))))

;; The behavior described below where `definterface` forms are not permitted
;; to be used in `deftype` and `defrecord` forms when they are not top-level
;; is a bug, documented in the following issue:
;;
;; https://github.com/chrisrink10/basilisp/issues/376
(python/tuple $))]
(->> (:methods opt-map)
(map (fn [[method-name args docstring]]
(let [total-arity (count args)
is-variadic? (let [[_ [amp rest-arg]] (split-with #(not= '& %) args)]
(and (= '& amp) (not (nil? rest-arg))))
fixed-arity (cond-> total-arity is-variadic? (- 2))]
{:method-name method-name
:args args
:fixed-arity fixed-arity
:is-variadic? is-variadic?
:docstring docstring
:python-name (->> (if is-variadic? "_rest" fixed-arity)
(str "_" (munge method-name) "_arity"))})))
(group-by :method-name)
(mapcat (fn [[method-name arities]]
(if (> (count arities) 1)
(let [fixed-arities (->> arities
(remove :is-variadic?)
(map :fixed-arity))
variadic-arities (filter :is-variadic? arities)
fixed-arity-for-variadic (some-> variadic-arities first :fixed-arity)
num-variadic (count variadic-arities)]
(when (not= (count (set fixed-arities))
(count fixed-arities))
(throw
(ex-info (str "Interface methods may not have multiple methods "
"with the same fixed arity")
{:arities arities
:fixed-arities (vec fixed-arities)})))
(when (> num-variadic 1)
(throw
(ex-info "Interface methods may have at most one variadic arity"
{:arities arities
:num-variadic num-variadic})))
(when (and fixed-arity-for-variadic
(some #(< fixed-arity-for-variadic %) fixed-arities))
(throw
(ex-info (str "Interface methods may not have a fixed arity "
"greater than the arity of a variadic method")
{:arities arities
:fixed-arity-for-variadic fixed-arity-for-variadic
:fixed-arities fixed-arities})))
(conj arities
{:method-name method-name
:python-name (munge method-name)
:args '[& args]
:docstring (-> arities first :docstring)}))
;; single arity methods should not have the special arity
;; python name
(map (fn [{:keys [method-name] :as arity}]
(assoc arity :python-name (munge method-name)))
arities))))
(reduce (fn [m {:keys [method-name python-name args docstring]}]
(let [method (->> args
(map #(vary-meta % assoc :no-warn-when-unused true))
(concat ['^:no-warn-when-unused self])
(apply vector)
(list 'fn* method-name)
(eval)
(abc/abstractmethod))]
(when docstring
(set! (.- method __doc__) docstring))
(assoc m python-name method)))
{})
(lisp->py)
(python/type interface-name extends))))

(defmacro definterface
"Define a new Python interface (abstract base clase) with the given name
Expand All @@ -4527,11 +4572,8 @@
definitions are declared as `abc.abstractmethod`s and thus must be implemented
by a concrete type.

The generated interface will be immediately available after this form has
been evaluated _if it is evaluated as a top-level form_. If this form appears
within a function and is referred to later in a `deftype` or `defrecord` form,
a compilation error will occur since the compiler will be unable to evaluate
whether the generated value refers to an abstract class."
Interfaces created by `definterface` cannot be declared as properties, class
methods, or static methods, as with `deftype`."
[interface-name & methods]
(let [name-str (name interface-name)
method-sigs (map #(list 'quote %) methods)]
Expand Down
112 changes: 37 additions & 75 deletions src/basilisp/lang/compiler/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,12 @@
DefType,
DefTypeBase,
DefTypeClassMethod,
DefTypeClassMethodArity,
DefTypeMember,
DefTypeMethod,
DefTypeMethodArity,
DefTypeMethodArityBase,
DefTypeMethodBase,
DefTypeProperty,
DefTypePythonMember,
DefTypeStaticMethod,
DefTypeStaticMethodArity,
Do,
Fn,
FnArity,
Expand Down Expand Up @@ -125,6 +122,7 @@
VarRef,
Vector as VectorNode,
WithMeta,
deftype_or_reify_python_member_names,
)
from basilisp.lang.interfaces import IMeta, IRecord, ISeq, IType, IWithMeta
from basilisp.lang.runtime import Var
Expand Down Expand Up @@ -1038,7 +1036,7 @@ def __deftype_classmethod(
method_name: str,
args: vec.Vector,
kwarg_support: Optional[KeywordArgSupport] = None,
) -> DefTypeClassMethodArity:
) -> DefTypeClassMethod:
"""Emit a node for a :classmethod member of a `deftype*` form."""
with ctx.hide_parent_symbol_table(), ctx.new_symbol_table(
method_name, is_context_boundary=True
Expand Down Expand Up @@ -1068,7 +1066,7 @@ def __deftype_classmethod(
)
with ctx.new_func_ctx(FunctionContext.CLASSMETHOD), ctx.expr_pos():
stmts, ret = _body_ast(ctx, runtime.nthrest(form, 2))
method = DefTypeClassMethodArity(
method = DefTypeClassMethod(
form=form,
name=method_name,
params=vec.vector(param_nodes),
Expand Down Expand Up @@ -1228,7 +1226,7 @@ def __deftype_staticmethod(
method_name: str,
args: vec.Vector,
kwarg_support: Optional[KeywordArgSupport] = None,
) -> DefTypeStaticMethodArity:
) -> DefTypeStaticMethod:
"""Emit a node for a :staticmethod member of a `deftype*` form."""
with ctx.hide_parent_symbol_table(), ctx.new_symbol_table(
method_name, is_context_boundary=True
Expand All @@ -1238,7 +1236,7 @@ def __deftype_staticmethod(
)
with ctx.new_func_ctx(FunctionContext.STATICMETHOD), ctx.expr_pos():
stmts, ret = _body_ast(ctx, runtime.nthrest(form, 2))
method = DefTypeStaticMethodArity(
method = DefTypeStaticMethod(
form=form,
name=method_name,
params=vec.vector(param_nodes),
Expand All @@ -1262,7 +1260,7 @@ def __deftype_staticmethod(

def __deftype_or_reify_prop_or_method_arity( # pylint: disable=too-many-branches
ctx: AnalyzerContext, form: Union[llist.List, ISeq], special_form: sym.Symbol
) -> Union[DefTypeMethodArityBase, DefTypeProperty]:
) -> Union[DefTypeMethodArity, DefTypePythonMember]:
"""Emit either a `deftype*` or `reify*` property node or an arity of a `deftype*`
or `reify*` method.

Expand Down Expand Up @@ -1343,9 +1341,9 @@ def __deftype_or_reify_prop_or_method_arity( # pylint: disable=too-many-branche
def __deftype_or_reify_method_node_from_arities( # pylint: disable=too-many-branches
ctx: AnalyzerContext,
form: Union[llist.List, ISeq],
arities: List[DefTypeMethodArityBase],
arities: List[DefTypeMethodArity],
special_form: sym.Symbol,
) -> DefTypeMethodBase:
) -> DefTypeMember:
"""Roll all of the collected `deftype*` or `reify*` arities up into a single
method node."""
assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}
Expand All @@ -1354,13 +1352,6 @@ def __deftype_or_reify_method_node_from_arities( # pylint: disable=too-many-bra
fixed_arity_for_variadic: Optional[int] = None
num_variadic = 0
for arity in arities:
if fixed_arity_for_variadic is not None:
if arity.fixed_arity >= fixed_arity_for_variadic:
raise AnalyzerException(
f"{special_form} method may not have a method with fixed arity "
"greater than fixed arity of variadic function",
form=arity.form,
)
if arity.is_variadic:
if num_variadic > 0:
raise AnalyzerException(
Expand Down Expand Up @@ -1397,41 +1388,14 @@ def __deftype_or_reify_method_node_from_arities( # pylint: disable=too-many-bra
form=form,
)

max_fixed_arity = max(arity.fixed_arity for arity in arities)

if all(isinstance(e, DefTypeMethodArity) for e in arities):
return DefTypeMethod(
form=form,
name=arities[0].name,
max_fixed_arity=max_fixed_arity,
arities=vec.vector(arities), # type: ignore[arg-type]
is_variadic=num_variadic == 1,
env=ctx.get_node_env(),
)
elif all(isinstance(e, DefTypeClassMethodArity) for e in arities):
return DefTypeClassMethod(
form=form,
name=arities[0].name,
max_fixed_arity=max_fixed_arity,
arities=vec.vector(arities), # type: ignore[arg-type]
is_variadic=num_variadic == 1,
env=ctx.get_node_env(),
)
elif all(isinstance(e, DefTypeStaticMethodArity) for e in arities):
return DefTypeStaticMethod(
form=form,
name=arities[0].name,
max_fixed_arity=max_fixed_arity,
arities=vec.vector(arities), # type: ignore[arg-type]
is_variadic=num_variadic == 1,
env=ctx.get_node_env(),
)
else:
raise AnalyzerException(
"deftype* method arities must all be declared one of :classmethod, "
":property, :staticmethod, or none (for a standard method)",
form=form,
)
return DefTypeMethod(
form=form,
name=arities[0].name,
max_fixed_arity=max(arity.fixed_arity for arity in arities),
arities=vec.vector(arities),
is_variadic=num_variadic == 1,
env=ctx.get_node_env(),
)


def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-locals # noqa: MC0001
Expand Down Expand Up @@ -1484,10 +1448,10 @@ def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-loca
# keys to act as an ordered set of members we've seen. We don't want to register
# duplicates.
member_order = {}
methods: MutableMapping[
str, List[DefTypeMethodArityBase]
] = collections.defaultdict(list)
props: MutableMapping[str, DefTypeProperty] = {}
methods: MutableMapping[str, List[DefTypeMethodArity]] = collections.defaultdict(
list
)
py_members: MutableMapping[str, DefTypePythonMember] = {}
for elem in runtime.nthrest(form, 2):
if not isinstance(elem, ISeq):
raise AnalyzerException(
Expand All @@ -1497,24 +1461,29 @@ def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-loca

member = __deftype_or_reify_prop_or_method_arity(ctx, elem, special_form)
member_order[member.name] = True
if isinstance(member, DefTypeProperty):
if member.name in props:
if isinstance(
member, (DefTypeClassMethod, DefTypeProperty, DefTypeStaticMethod)
):
if member.name in py_members:
raise AnalyzerException(
f"{special_form} property may only have one arity defined",
f"{special_form} class methods, properties, and static methods "
"may only have one arity defined",
form=elem,
lisp_ast=member,
)
elif member.name in methods:
raise AnalyzerException(
f"{special_form} property name already defined as a method",
f"{special_form} class method, property, or static method name "
"already defined as a method",
form=elem,
lisp_ast=member,
)
props[member.name] = member
py_members[member.name] = member
else:
if member.name in props:
if member.name in py_members:
raise AnalyzerException(
f"{special_form} method name already defined as a property",
f"{special_form} method name already defined as a class method, "
"property, or static method",
form=elem,
lisp_ast=member,
)
Expand All @@ -1531,9 +1500,9 @@ def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-loca
)
continue

prop = props.get(member_name)
assert prop is not None, "Member must be a method or property"
members.append(prop)
py_member = py_members.get(member_name)
assert py_member is not None, "Member must be a method or property"
members.append(py_member)

return interfaces, members

Expand All @@ -1557,7 +1526,7 @@ def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-bran
assert special_form in {SpecialForm.DEFTYPE, SpecialForm.REIFY}

field_names = frozenset(fields)
member_names = frozenset(munge(member.name) for member in members)
member_names = frozenset(deftype_or_reify_python_member_names(members))
all_member_names = field_names.union(member_names)
all_interface_methods: Set[str] = set()
for interface in interfaces:
Expand Down Expand Up @@ -1916,13 +1885,6 @@ def _fn_ast( # pylint: disable=too-many-branches
fixed_arity_for_variadic: Optional[int] = None
num_variadic = 0
for arity in arities:
if fixed_arity_for_variadic is not None:
if arity.fixed_arity >= fixed_arity_for_variadic:
raise AnalyzerException(
"fn may not have a method with fixed arity greater than "
"fixed arity of variadic function",
form=arity.form,
)
if arity.is_variadic:
if num_variadic > 0:
raise AnalyzerException(
Expand Down
Loading