diff --git a/CHANGELOG.md b/CHANGELOG.md index bb7701fe8..236b59099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * 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) + * Added support for Protocols (#460) + * Added support for Volatiles (#460) ### 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) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 2a1b8e2f6..c9955329d 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -4187,7 +4187,8 @@ [arg] (throw (ex-info "Invalid destructuring argument type" - {:type (python/type arg)}))) + {:type (python/type arg) + :arg arg}))) (defmulti ^:private destructure-binding (fn [ddef] @@ -4493,10 +4494,11 @@ (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))] + (let [includes-self? (contains? #{'self 'this} (first args)) + total-arity (cond-> (count args) includes-self? (dec)) + 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 @@ -4533,20 +4535,21 @@ {:arities arities :fixed-arity-for-variadic fixed-arity-for-variadic :fixed-arities fixed-arities}))) + ;; multi-arity methods need to include an outer dispatch method, + ;; which will not be defined by the user and must be defined here (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 + ;; single arity methods should not have a private Python name + ;; since they do not have an outer dispatch method (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) @@ -4562,7 +4565,8 @@ "Define a new Python interface (abstract base clase) with the given name and method signatures. - Method signatures are in the form (method-name [arg1 arg2 ...]). + Method signatures are in the form (method-name [arg1 arg2 ...]). `self` or + `this` arguments must be omitted from all interfaces. Interface objects cannot be directly instantiated. Instead, you must create a concrete implementation of an interface (perhaps via deftype) and supply @@ -4576,11 +4580,361 @@ methods, or static methods, as with `deftype`." [interface-name & methods] (let [name-str (name interface-name) - method-sigs (map #(list 'quote %) methods)] + method-sigs (->> methods + (map (fn [[method-name args docstring]] + [method-name (conj args 'self) docstring])) + (map #(list 'quote %)))] `(def ~interface-name (gen-interface :name ~name-str :methods [~@method-sigs])))) +;;;;;;;;;;;;;;; +;; Protocols ;; +;;;;;;;;;;;;;;; + +(import* functools) + +(defn ^:private gen-protocol-dispatch + "Return the dispatch function for a single protocol method." + [protocol-name interface-name [method-name & args+docstring :as method-def]] + (let [[arglists [docstring]] (split-with (complement string?) args+docstring)] + [`(def ~(vary-meta method-name + assoc + :doc docstring + :arglists (list 'quote arglists)) + (functools/singledispatch + (fn ~method-name + ~@(map (fn [[obj-sym & args]] + (list `[~obj-sym ~@(map #(vary-meta % assoc :no-warn-when-unused true) args)] + `(let [obj-type# (python/type ~obj-sym)] + (throw + (ex-info (str + ~(str "No implementation of method " method-name " found for type ") + obj-type#) + {:protocol (var ~protocol-name) + :method ~method-name + :object-type obj-type#}))))) + arglists)))) + (let [dotted-method-name (symbol (str "." (name method-name)))] + `(.register ~method-name + ~interface-name + (fn ~method-name + ~@(map (fn [[obj-sym & args]] + (let [has-varargs (some #(= '& %) args) + clean-args (filter #(not= '& %) args)] + (list `[~obj-sym ~@args] + (if has-varargs + `(apply-method ~obj-sym ~method-name ~@clean-args) + `(~dotted-method-name ~obj-sym ~@clean-args))))) + arglists))))])) + +;; For each protocol, the following keys are defined: +;; +;; - :impls - a set of types which dynamically extend the Protocol +;; (e.g. by `extend`) +;; - :interface - the Python interface type which backs the Protocol +;; - :methods - a mapping of method names (as Basilisp keywords) to the dispatch +;; method backing the Protocol method +;; - :var - a reference to the Var the Protocol is stored in + +(defmacro defprotocol + "Define a new protocol. + + Protocols are similar to classical programming interfaces. Rather than defining an + interface and implementing that interface on different objects, protocols generate + a set of functions which dispatch to the correct implementation on the type of their + first argument (which is similar to the Python `self` argument). + + Also like interfaces, Protocols do not include implementations for any of their member + methods. Instead, implementations can be provided using `deftype`, `defrecord`, + `reify`, or `extend` (or any of the `extend-*` macros). + + Despite their differences from interfaces, Protocols do also generate a standard + Python interface type (deriving from `abc.ABC`) which is used for efficient dispatch + for implementing types. + + Method signatures are in the form: + + (method-name [self arg1] [self arg1 arg2] \"This method foos the bars.\") + + Callers must provide the `self` or `this` arguments for each method." + [protocol-name & methods] + (let [doc (when (string? (first methods)) + (first methods)) + methods (if doc (rest methods) methods) + protocol-name (vary-meta + (cond-> protocol-name + doc (vary-meta assoc :doc doc)) + assoc + :protocol true + :redef true) + interface-sym (gensym "interface-name")] + `(do + (defonce ~protocol-name {}) + (alter-meta! (var ~protocol-name) + assoc + :doc ~doc + :basilisp.core/protocol true) + (let [~interface-sym (gen-interface :name ~(name protocol-name) + :methods [~@(mapcat + (fn [[method-name & args+docstring]] + (let [[arglists [docstring]] (split-with + (complement string?) + args+docstring)] + (map (fn [args] + (list 'quote + `(~method-name + ~args + ~@(when docstring [docstring])))) + arglists))) + methods)])] + ~@(mapcat #(gen-protocol-dispatch protocol-name interface-sym %) methods) + (->> {:impls #{} + :interface ~interface-sym + :methods ~(apply hash-map + (mapcat #(let [v (first %)] + [(keyword (name v)) v]) + methods)) + :var (var ~protocol-name)} + (alter-var-root (var ~protocol-name) merge ~protocol-name))) + ~protocol-name))) + +(defn ^:private protocol? + "Return true if x is a Protocol." + [x] + (boolean (and (map? x) (:interface x)))) + +(defn extend + "Extend a type dynamically with one or Protocol implementations. This is useful + for types which were defined outside your control or which were not otherwise + defined as direct implementors of the named Protocol(s). With `extend`, such + types may be extended without modifying the types directly. + + `target-type` should name a type which is to be extended. Virtually any type + is permitted, so long as that that type is not also a Protocol. If `target-type` + is an interface, objects of types implementing that interface will be extended + by the given implementation unless a more concrete implementation is available + (by the Python method resolution order of the object's type). If `target-type` + is `nil`, it will be interpreted as `(python/type nil)`. + + `proto+methods` are interleaved Protocol names and maps of Protocol method + implementations intended for `target-type`. The keys of each map are keywords + corresponding to the names of the Protocol methods (as defined in the + `defprotocol`). Each value should be either an existing function (referenced + by its Var or name binding) or a new function definition. + + `extend` may be called multiple times for a single `target-type`, so not all + implementations need to be known a priori. Successive calls to `extend` on + `target-type` with the same Protocol implementations will overwrite previous + implementations. + + If you are extending types with explicit function definitions, the + `extend-protocol` and `extend-type` macros offer some additional conveniences. + + Returns `nil`. + + Example: + + (extend SomeType + FirstProto + {:spam spam-fn + :eggs (fn [...] ...)} + OtherProto + {:ham (fn [...] ...)})" + [target-type & proto+methods] + (let [target-type (cond-> target-type (nil? target-type) (python/type))] + (cond + (protocol? target-type) + (throw (ex-info "Protocols may not be extended with other Protocols" + {:target-type target-type})) + + (not (instance? python/type target-type)) + (throw (ex-info "extend target-type must be a type" + {:target-type target-type}))) + + (doseq [proto-pair (apply hash-map proto+methods) + :let [[proto method-map] proto-pair]] + (when-not (protocol? proto) + (throw (ex-info "extend protocol must be protocol as defined by defprotocol" + {:target-type target-type + :proto proto}))) + (when (identical? target-type proto) + (throw (ex-info "Protocol may not extend itself" + {:target-type target-type + :proto proto}))) + + (->> (fn [old-root] + (let [proto-methods (:methods proto)] + (let [proto-method-names (set (keys proto-methods)) + impl-method-names (set (keys method-map))] + (when-not (= proto-method-names impl-method-names) + (throw (ex-info "target-type must implement all protocol methods" + {:missing-methods (->> proto-method-names + (remove impl-method-names) + (set))})))) + (let [proto-method-arity-map (reduce-kv (fn [m k v] + (assoc m k (.-arities v))) + {} + proto-methods) + impl-method-arity-map (reduce-kv (fn [m k v] + (assoc m k (.-arities v))) + {} + method-map)] + (doseq [proto-method proto-method-arity-map + :let [[proto-method-name proto-method-arities] proto-method]] + (let [impl-method-arities (get impl-method-arity-map proto-method-name)] + (when (not= impl-method-arities proto-method-arities) + (throw (ex-info "target-type must implement all protocol method arities" + {:proto-method proto-method-name + :proto-method-arities proto-method-arity-map + :impl-method-arities impl-method-arity-map + :missing-arities (->> proto-method-arities + (remove impl-method-arities) + (set))})))))) + (doseq [method-def method-map + :let [[method-name fn] method-def]] + (let [dispatch-method (get proto-methods method-name)] + (.register dispatch-method target-type fn)))) + (update old-root :impls conj target-type)) + (alter-var-root (:var proto))))) + + nil) + +(defn ^:private sym-and-method-groups + "Group methods for `extend-protocol` and `extend-type` into a map of interface + names to a vector of method bodies." + [specs] + (loop [iface (first specs) + specs (rest specs) + methods [] + groups {}] + (cond + (not (seq specs)) + (assoc groups iface methods) + + (list? (first specs)) + (recur iface + (rest specs) + (conj methods (first specs)) + groups) + + (or (symbol? (first specs)) (nil? (first specs))) + (recur (first specs) + (rest specs) + [] + (assoc groups iface methods))))) + +(defn ^:private extend-map + "Convert a vector of method definitions (as expected by `extend-protocol` and + `extend-type`) into a map of method definitions which can be passed to `extend`. " + [methods] + (->> (group-by first methods) + (reduce (fn [m [method-name arities]] + (->> (map rest arities) + (apply list `fn method-name) + (assoc m (keyword (name method-name))))) + {}))) + +(defmacro extend-protocol + "Extend a Protocol with implementations for multiple types. + + This convenience macro is useful for extending multiple different types with + a single Protocol definition in one call. Note that this macro only supports + new function declarations. If you intend to reference an existing function, + call `extend` directly. + + For example, this call: + + (extend-protocol SomeProto + FirstType + (spam + ([this] ...) + ([this arg] ...)) + (eggs [this arg1 arg2] ...) + OtherType + (spam + ([this] ...) + ([this arg] ...)) + (eggs [this arg1 arg2] ...)) + + Would be turned into the following `extend` calls: + + (do + (extend FirstType + SomeProto + {:spam (fn spam + ([this] ...) + ([this arg] ...)) + :eggs (fn [this arg1 arg2] ...)}) + (extend OtherType + SomeProto + {:spam (fn spam + ([this] ...) + ([this arg] ...)) + :eggs (fn [this arg1 arg2] ...)}))" + [proto & specs] + `(do ~@(map (fn [[target-type methods]] + `(extend ~target-type + ~proto + ~(extend-map methods))) + (sym-and-method-groups specs)))) + +(defmacro extend-type + "Extend a type with multiple Protocol implementations. + + This convenience macro is useful for extending a single type with multiple + different Protocol definitions in one call. Note that this macro only supports + new function declarations. If you intend to reference an existing function, + call `extend` directly. + + For example, this call: + + (extend-type SomeType + FirstProto + (spam + ([this] ...) + ([this arg] ...)) + (eggs [this arg1 arg2] ...) + SecondProto + (ham [this & args] ...)) + + Would be turned into the following `extend` call: + + (extend SomeType + FirstProto + {:spam (fn spam + ([this] ...) + ([this arg] ...)) + :eggs (fn eggs [this arg1 arg2] ...)} + SecondProto + {:ham (fn ham [this & args] ...)})" + [target-type & specs] + `(extend ~target-type + ~@(mapcat (fn [[proto methods]] + [proto (extend-map methods)]) + (sym-and-method-groups specs)))) + +(defn extenders + "Return a collection of types explicitly extending proto. + + The returned collection will not include types which extend proto via + inheritance (as by `deftype` and `defrecord`). Only types extending + proto via `extend` (or `extend-protocol` or `extend-type`) will appear." + [{:keys [impls] :as proto}] + (apply list impls)) + +(defn extends? + "Return true if type extends protocol proto." + [{:keys [interface impls] :as proto} type] + (or (python/issubclass type interface) + (contains? impls type))) + +(defn satisfies? + "Return true if x satisfies protocol proto." + [{:keys [interface impls] :as proto} x] + (or (instance? interface x) + (python/isinstance x (python/tuple impls)))) + ;;;;;;;;;;;;;;;; ;; Data Types ;; ;;;;;;;;;;;;;;;; @@ -4996,3 +5350,39 @@ ~type-name))) +;;;;;;;;;;;;;;; +;; Volatiles ;; +;;;;;;;;;;;;;;; + +(defprotocol IVolatile + "Protocol for a volatile reference container. Volatile references do not provide + atomic semantics, but they may be useful as a mutable reference container in a + single-threaded context." + + (vreset! [this new-val] + "Reset the value of a volatile non-atomically. Returns the new value.") + (vswap! [this f & args] + "Swap the value of a volatile non-atomically to the return of + (apply f old-val args). Returns the new value of that function + call.")) + +(deftype Volatile [^:mutable val] + basilisp.lang.interfaces/IDeref + (deref [this] val) + + IVolatile + (vreset! [this new-val] + (set! val new-val)) + (vswap! [this f & args] + (let [new-val (apply f val args)] + (set! val new-val)))) + +(defn volatile! + "Return a Volatile reference container with the initial value v." + [v] + (Volatile v)) + +(defn volatile? + "Return true if implements IVolatile." + [x] + (satisfies? IVolatile x)) diff --git a/src/basilisp/lang/compiler/analyzer.py b/src/basilisp/lang/compiler/analyzer.py index e9ec1bfe6..200bb6221 100644 --- a/src/basilisp/lang/compiler/analyzer.py +++ b/src/basilisp/lang/compiler/analyzer.py @@ -61,6 +61,7 @@ SYM_PRIVATE_META_KEY, SYM_PROPERTY_META_KEY, SYM_STATICMETHOD_META_KEY, + VAR_IS_PROTOCOL_META_KEY, SpecialForm, ) from basilisp.lang.compiler.exception import CompilerException, CompilerPhase @@ -150,6 +151,7 @@ # Constants used in analyzing AS = kw.keyword("as") IMPLEMENTS = kw.keyword("implements") +INTERFACE = kw.keyword("interface") STAR_STAR = sym.symbol("**") _DOUBLE_DOT_MACRO_NAME = ".." _BUILTINS_NS = "python" @@ -1507,6 +1509,9 @@ def __deftype_or_reify_impls( # pylint: disable=too-many-branches,too-many-loca return interfaces, members +_var_is_protocol = _meta_getter(VAR_IS_PROTOCOL_META_KEY) + + def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-branches,too-many-locals special_form: sym.Symbol, fields: Iterable[str], @@ -1543,7 +1548,15 @@ def __deftype_and_reify_impls_are_all_abstract( # pylint: disable=too-many-bran "and cannot be checked for abstractness; deferring to runtime", ) return False - interface_type = interface.var.value + + # Protocols are defined as maps, with the interface being simply a member + # of the map, denoted by the keyword `:interface`. + if _var_is_protocol(interface.var): + proto_map = interface.var.value + assert isinstance(proto_map, lmap.Map) + interface_type = proto_map.val_at(INTERFACE) + else: + interface_type = interface.var.value if interface_type is object: continue diff --git a/src/basilisp/lang/compiler/constants.py b/src/basilisp/lang/compiler/constants.py index dd9755544..76be9b551 100644 --- a/src/basilisp/lang/compiler/constants.py +++ b/src/basilisp/lang/compiler/constants.py @@ -53,3 +53,5 @@ class SpecialForm: LINE_KW = kw.keyword("line") NAME_KW = kw.keyword("name") NS_KW = kw.keyword("ns") + +VAR_IS_PROTOCOL_META_KEY = kw.keyword("protocol", "basilisp.core") diff --git a/src/basilisp/lang/compiler/generator.py b/src/basilisp/lang/compiler/generator.py index 16ab57449..b8d4a9d0f 100644 --- a/src/basilisp/lang/compiler/generator.py +++ b/src/basilisp/lang/compiler/generator.py @@ -41,6 +41,7 @@ SYM_DYNAMIC_META_KEY, SYM_NO_WARN_ON_REDEF_META_KEY, SYM_REDEF_META_KEY, + VAR_IS_PROTOCOL_META_KEY, ) from basilisp.lang.compiler.exception import CompilerException, CompilerPhase from basilisp.lang.compiler.nodes import ( @@ -1266,6 +1267,46 @@ def __deftype_member_to_py_ast( return handle_deftype_member(ctx, node) +def __deftype_or_reify_bases_to_py_ast( + ctx: GeneratorContext, node: Union[DefType, Reify] +) -> List[ast.AST]: + """Return a list of AST nodes for the base classes for a `deftype*` or `reify*`.""" + assert node.op in {NodeOp.DEFTYPE, NodeOp.REIFY} + + bases: List[ast.AST] = [] + for base in node.interfaces: + base_node = gen_py_ast(ctx, base) + assert ( + count(base_node.dependencies) == 0 + ), "Class and host form nodes do not have dependencies" + + # Protocols are defined as Maps + if ( + isinstance(base, VarRef) + and base.var.meta is not None + and base.var.meta.val_at(VAR_IS_PROTOCOL_META_KEY) + ): + bases.append( + ast.Call( + func=ast.Attribute( + value=base_node.node, attr="val_at", ctx=ast.Load() + ), + args=[ + ast.Call( + func=_NEW_KW_FN_NAME, + args=[ast.Constant("interface")], + keywords=[], + ) + ], + keywords=[], + ) + ) + else: + bases.append(base_node.node) + + return bases + + @_with_ast_loc def _deftype_to_py_ast( # pylint: disable=too-many-branches,too-many-locals ctx: GeneratorContext, node: DefType @@ -1275,13 +1316,7 @@ def _deftype_to_py_ast( # pylint: disable=too-many-branches,too-many-locals type_name = munge(node.name) ctx.symbol_table.new_symbol(sym.symbol(node.name), type_name, LocalType.DEFTYPE) - bases = [] - for base in node.interfaces: - base_node = gen_py_ast(ctx, base) - assert ( - count(base_node.dependencies) == 0 - ), "Class and host form nodes do not have dependencies" - bases.append(base_node.node) + bases = __deftype_or_reify_bases_to_py_ast(ctx, node) with ctx.new_symbol_table(node.name): fields = [] @@ -1469,6 +1504,35 @@ def __fn_args_to_py_ast( return fn_args, varg, fn_body_ast +def __fn_decorator(arities: Iterable[int], has_rest_arg: bool = False,) -> ast.Call: + return ast.Call( + func=_BASILISP_FN_FN_NAME, + args=[], + keywords=[ + ast.keyword( + arg="arities", + value=ast.Tuple( + elts=list( + chain( + map(ast.Constant, arities), + [ + ast.Call( + func=_NEW_KW_FN_NAME, + args=[ast.Constant("rest")], + keywords=[], + ) + ] + if has_rest_arg + else [], + ) + ), + ctx=ast.Load(), + ), + ) + ], + ) + + def __fn_meta( ctx: GeneratorContext, meta_node: Optional[MetaNode] = None ) -> Tuple[Iterable[ast.AST], Iterable[ast.AST]]: @@ -1549,7 +1613,14 @@ def __single_arity_fn_to_py_ast( chain( __kwargs_support_decorator(node), meta_decorators, - [_BASILISP_FN_FN_NAME], + [ + __fn_decorator( + (len(fn_args),) + if not method.is_variadic + else (), + has_rest_arg=method.is_variadic, + ) + ], [_TRAMPOLINE_FN_NAME] if ctx.recur_point.has_recur else [], @@ -1714,7 +1785,17 @@ def fn(*args): kw_defaults=[], ), body=body, - decorator_list=list(chain(meta_decorators, [_BASILISP_FN_FN_NAME])), + decorator_list=list( + chain( + meta_decorators, + [ + __fn_decorator( + arity_map.keys(), + has_rest_arg=default_name is not None, + ) + ], + ) + ), returns=None, ) ], @@ -2291,14 +2372,10 @@ def _reify_to_py_ast( else: meta_ast = None - bases: List[ast.AST] = [_BASILISP_WITH_META_INTERFACE_NAME] - for base in node.interfaces: - base_node = gen_py_ast(ctx, base) - assert ( - count(base_node.dependencies) == 0 - ), "Class and host form nodes do not have dependencies" - bases.append(base_node.node) - + bases: List[ast.AST] = [ + _BASILISP_WITH_META_INTERFACE_NAME, + *__deftype_or_reify_bases_to_py_ast(ctx, node), + ] type_name = munge(genname("ReifiedType")) with ctx.new_symbol_table("reify"): diff --git a/src/basilisp/lang/list.py b/src/basilisp/lang/list.py index 8cb8f5b42..e187f2cbe 100644 --- a/src/basilisp/lang/list.py +++ b/src/basilisp/lang/list.py @@ -10,7 +10,7 @@ T = TypeVar("T") -class List(IWithMeta, ISeq[T], IPersistentList[T]): +class List(IPersistentList[T], ISeq[T], IWithMeta): """Basilisp List. Delegates internally to a pyrsistent.PList object. Do not instantiate directly. Instead use the l() and list() factory diff --git a/src/basilisp/lang/map.py b/src/basilisp/lang/map.py index decff20da..19e3467ad 100644 --- a/src/basilisp/lang/map.py +++ b/src/basilisp/lang/map.py @@ -24,7 +24,7 @@ _ENTRY_SENTINEL = object() -class Map(ILispObject, IWithMeta, IPersistentMap[K, V]): +class Map(IPersistentMap[K, V], ILispObject, IWithMeta): """Basilisp Map. Delegates internally to a pyrsistent.PMap object. Do not instantiate directly. Instead use the m() and map() factory methods below.""" diff --git a/src/basilisp/lang/runtime.py b/src/basilisp/lang/runtime.py index c22c87968..86fdbf447 100644 --- a/src/basilisp/lang/runtime.py +++ b/src/basilisp/lang/runtime.py @@ -418,6 +418,7 @@ class Namespace(ReferenceBase): [ "attr", "builtins", + "functools", "io", "importlib", "operator", @@ -1149,7 +1150,7 @@ def contains(coll, k): def get(m, k, default=None): """Return the value of k in m. Return default if k not found in m.""" if isinstance(m, ILookup): - return m.val_at(k, default=default) + return m.val_at(k, default) try: return m[k] @@ -1453,14 +1454,19 @@ def wrapped_f(*args, **kwargs): return wrapped_f -def _basilisp_fn(f): +def _basilisp_fn(arities: Tuple[Union[int, kw.Keyword]]): """Create a Basilisp function, setting meta and supplying a with_meta method implementation.""" - assert not hasattr(f, "meta") - f._basilisp_fn = True - f.meta = None - f.with_meta = partial(_fn_with_meta, f) - return f + + def wrap_fn(f): + assert not hasattr(f, "meta") + f._basilisp_fn = True + f.arities = lset.set(arities) + f.meta = None + f.with_meta = partial(_fn_with_meta, f) + return f + + return wrap_fn def _basilisp_type( diff --git a/src/basilisp/lang/set.py b/src/basilisp/lang/set.py index 17b4f49a8..9c3c5bf04 100644 --- a/src/basilisp/lang/set.py +++ b/src/basilisp/lang/set.py @@ -15,7 +15,7 @@ T = TypeVar("T") -class Set(IWithMeta, ILispObject, IPersistentSet[T]): +class Set(IPersistentSet[T], ILispObject, IWithMeta): """Basilisp Set. Delegates internally to a pyrsistent.PSet object. Do not instantiate directly. Instead use the s() and set() factory diff --git a/src/basilisp/lang/vector.py b/src/basilisp/lang/vector.py index 324afe370..0659a2dcd 100644 --- a/src/basilisp/lang/vector.py +++ b/src/basilisp/lang/vector.py @@ -16,7 +16,7 @@ T = TypeVar("T") -class Vector(ILispObject, IWithMeta, IPersistentVector[T]): +class Vector(IPersistentVector[T], ILispObject, IWithMeta): """Basilisp Vector. Delegates internally to a pyrsistent.PVector object. Do not instantiate directly. Instead use the v() and vec() factory methods below.""" diff --git a/src/basilisp/walk.lpy b/src/basilisp/walk.lpy index b8442e3d9..90b88a8f6 100644 --- a/src/basilisp/walk.lpy +++ b/src/basilisp/walk.lpy @@ -1,5 +1,46 @@ (ns basilisp.walk) +(defprotocol IWalkable + (walk* [this inner outer] + "Walk the data structure applying `inner` to each element of the data structure, + if any, and then applying `outer` to the result.")) + +(extend-protocol IWalkable + basilisp.lang.interfaces/IPersistentList + (walk* [this inner outer] + (outer (apply list (map inner this)))) + + basilisp.lang.interfaces/IMapEntry + (walk* [this inner outer] + (outer (map-entry (inner (key this)) (inner (val this))))) + + basilisp.lang.interfaces/ISeq + (walk* [this inner outer] + (outer (doall (map inner this)))) + + basilisp.lang.interfaces/IPersistentVector + (walk* [this inner outer] + (outer (apply vector (map inner this)))) + + basilisp.lang.interfaces/IPersistentMap + (walk* [this inner outer] + (outer (apply hash-map (mapcat inner this)))) + + basilisp.lang.interfaces/IPersistentSet + (walk* [this inner outer] + (outer (apply hash-set (map inner this)))) + + basilisp.lang.interfaces/IRecord + (walk* [this inner outer] + (outer (reduce (fn [rec field] + (conj rec (inner field))) + this + this))) + + python/object + (walk* [this ^:no-warn-when-unused inner outer] + (outer this))) + (defn walk "Walk an arbitrary, possibly nested data structure, applying inner to each element of form and then applying outer to the resulting form. @@ -8,33 +49,7 @@ Lazy sequences will be completely consumed (and thus may not be infinite)." [inner outer form] - (cond - (list? form) - (outer (apply list (map inner form))) - - (map-entry? form) - (outer (map-entry (inner (key form)) (inner (val form)))) - - (seq? form) - (outer (doall (map inner form))) - - (vector? form) - (outer (apply vector (map inner form))) - - (map? form) - (outer (apply hash-map (mapcat inner form))) - - (set? form) - (outer (apply hash-set (map inner form))) - - (record? form) - (outer (reduce (fn [rec field] - (conj rec (inner field))) - form - form)) - - :else - (outer form))) + (walk* form inner outer)) (defn postwalk "Walk form using depth-first, post-order traversal, applying f to each form diff --git a/tests/basilisp/core_fns_test.lpy b/tests/basilisp/core_fns_test.lpy index 27456433a..efa10201f 100644 --- a/tests/basilisp/core_fns_test.lpy +++ b/tests/basilisp/core_fns_test.lpy @@ -337,6 +337,10 @@ (is (= "lo w" (subs "hello world" 3 7))) (is (thrown? python/IndexError (subs "hello world" 12 3)))) +;;;;;;;;;;;;; +;; Futures ;; +;;;;;;;;;;;;; + (deftest futures-test (testing "successful future" (let [fut (future 1)] @@ -391,6 +395,10 @@ (slow 3) (slow 4)))))))) +;;;;;;;;;;;; +;; Arrays ;; +;;;;;;;;;;;; + (deftest to-array-test (is (= #py [] (to-array []))) (is (= #py [] (to-array '()))) @@ -507,6 +515,10 @@ (is (= #py [#py [:a :b 5] #py [:d :e :f]] l)) (is (thrown? python/IndexError (aset l 0 5 :cc))))) +;;;;;;;;;;;;;;;; +;; Interfaces ;; +;;;;;;;;;;;;;;;; + (deftest gen-interface-test (are [x] (thrown? basilisp.lang.exception/ExceptionInfo x) (gen-interface :name "TestInterface" @@ -534,3 +546,20 @@ (other [arg]) (other [arg1 arg2 arg3]) (other [arg1 arg2 arg3 & rest-arg])])) + +;;;;;;;;;;;;;;; +;; Volatiles ;; +;;;;;;;;;;;;;;; + +(deftest volatile-test + (let [v (volatile! :a)] + (is (satisfies? IVolatile v)) + (is (not (satisfies? IVolatile @v))) + (is (not (volatile? :a))) + (is (not (volatile? (atom :a)))) + (is (volatile? v)) + (is (= :a @v)) + (is (= :b (vreset! v :b))) + (is (= :b @v)) + (is (= :b/a (vswap! v #(keyword (name %) "a")))) + (is (= :b/a @v)))) diff --git a/tests/basilisp/core_test.py b/tests/basilisp/core_test.py index 2d4b8d411..406059ab0 100644 --- a/tests/basilisp/core_test.py +++ b/tests/basilisp/core_test.py @@ -525,7 +525,7 @@ def test_truth_values_are_not_false(self, truthy_value): class TestIsFn: @pytest.fixture(scope="class") def basilisp_fn(self): - @runtime._basilisp_fn + @runtime._basilisp_fn(arities=(1,)) def repeat(v): while True: yield v diff --git a/tests/basilisp/test_protocols.lpy b/tests/basilisp/test_protocols.lpy new file mode 100644 index 000000000..490e3c548 --- /dev/null +++ b/tests/basilisp/test_protocols.lpy @@ -0,0 +1,188 @@ +(ns tests.basilisp.test-protocols + (:import math) + (:require + [basilisp.string :as str] + [basilisp.test :refer [deftest is are testing]])) + +(defprotocol Shape + (area [this])) + +(defprotocol Polygon + (interior-angle [this] + "Return the interior angle of the shape in degrees.")) + +(defrecord Circle [radius] + Shape + (area [this] (* math/pi radius radius))) + +(defrecord Rectangle [x y] + Shape + (area [this] (* x y)) + Polygon + (interior-angle [this] 360)) + +(defrecord Square [x]) + +(extend-type Square + Shape + (area [this] + (let [side (get this :x)] + (* side side)))) + +(defprotocol Describable + (self-name [this] [this upper-case?]) + (describe-me [this])) + +(extend-protocol Describable + python/int + (self-name [this] "int") + (self-name [this upper-case?] + (cond-> (self-name this) + upper-case? (str/upper-case))) + (describe-me [this] + (cond + (pos? this) "I am a positive integer." + (zero? this) "I am zero." + (neg? this) "I am a negative integer.")) + python/str + (self-name [this] "str") + (self-name [this upper-case?] + (cond-> (self-name this) + upper-case? (str/upper-case))) + (describe-me [this] (str "I am a string of length " (count this) ".")) + nil + (self-name [this] "nil") + (self-name [this upper-case?] + (cond-> (self-name this) + upper-case? (str/upper-case))) + (describe-me [this] "I am the value nil.")) + +(deftest Shape-area-method-test + (is (= math/pi (area (->Circle 1)))) + (is (= 4 (area (->Rectangle 2 2)))) + (is (= 9 (area (->Square 3)))) + + (is (thrown? basilisp.lang.exception/ExceptionInfo + (area :a))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (area [4 5])))) + +(deftest Polygon-interior-angle-test + (is (= 360 (interior-angle (->Rectangle 2 2)))) + + (is (thrown? basilisp.lang.exception/ExceptionInfo + (interior-angle (->Circle 1)))) + (is (thrown? basilisp.lang.exception/ExceptionInfo + (interior-angle (->Square 3))))) + +(deftest Describable-self-name-method-test + (are [x y] (= x (self-name y)) + "str" "some-value" + "str" "" + "nil" nil + "int" 1 + "int" true + "int" 0 + "int" false + "int" -1) + + (are [x y] (= x (self-name y true)) + "STR" "some-value" + "STR" "" + "NIL" nil + "INT" 1 + "INT" true + "INT" 0 + "INT" false + "INT" -1) + + (are [x] (thrown? basilisp.lang.exception/ExceptionInfo (self-name x)) + -1.5 + :kw + 'sym + [] + #{} + {} + '()) + + (are [x] (thrown? basilisp.lang.exception/ExceptionInfo (self-name x true)) + -1.5 + :kw + 'sym + [] + #{} + {} + '())) + +(deftest Describable-describe-me-method-test + (are [x y] (= x (describe-me y)) + "I am a string of length 10." "some-value" + "I am the value nil." nil + "I am a positive integer." 1 + "I am a positive integer." true + "I am zero." 0 + nil false ;; (zero? false) => false + "I am a negative integer." -1) + + (are [x] (thrown? basilisp.lang.exception/ExceptionInfo (describe-me x)) + -1.5 + :kw + 'sym + [] + #{} + {} + '())) + +(deftest protocol?-test + (is (protocol? Shape)) + (is (not (protocol? Circle))) + (is (not (protocol? Rectangle)))) + +(deftest extend-test + (testing "Protocols cannot extend other Protocols" + (are [x] (thrown? basilisp.lang.exception/ExceptionInfo x) + (extend Polygon Shape {:area identity}) + (extend-protocol Polygon + Shape + (area [this] this)))) + + (testing "Protocol must be defined as by defprotocol" + (are [x] (thrown? basilisp.lang.exception/ExceptionInfo x) + (extend Square python/str {:area identity}) + (extend-type Square + python/str + (area [this] this)))) + + (testing "Protocols cannot extend themselves" + (are [x] (thrown? basilisp.lang.exception/ExceptionInfo x) + (extend Shape Shape {:area identity}) + (extend-protocol Shape + Shape + (area [this] this))) + + (testing "extended type must implement all Protocol methods" + (are [x] (thrown? basilisp.lang.exception/ExceptionInfo x) + (extend python/str Shape {}) + (extend-type python/float + Describable + (self-name [this] "float")) + (extend-type python/float + Describable + (self-name [this] "float") + (describe-me [this] "I am a float.")))))) + +(deftest extenders-test + (is (= (list Square) (extenders Shape))) + (is (not (seq (extenders Polygon))))) + +(deftest extends?-test + (is (extends? Shape Circle)) + (is (extends? Shape Rectangle)) + (is (not (extends? Polygon Circle))) + (is (extends? Polygon Rectangle))) + +(deftest satisfies?-test + (is (satisfies? Shape (->Circle 1))) + (is (satisfies? Shape (->Rectangle 2 2))) + (is (not (satisfies? Polygon (->Circle 1)))) + (is (satisfies? Polygon (->Rectangle 2 2))))