diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d52d2ab..fa5ba6748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added * Added support for the `:param-tags` reader metadata syntax `^[tag ...]` from Clojure 1.12 (#1111) + * Add support for tagged literals (#1104) ### Changed * Types generated by `reify` may optionally be marked as `^:mutable` now to prevent `attrs.exceptions.FrozenInstanceError`s being thrown when mutating methods inherited from the supertype(s) are called (#1088) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 8428226ab..101ea2382 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -547,6 +547,17 @@ v (.-name v))) +(defn ^:inline tagged-literal + "Construct a data representation of a tagged literal from a + tag symbol and a form." + [tag form] + (basilisp.lang.tagged/tagged-literal tag form)) + +(defn ^:inline tagged-literal? + "Return true if the value is the data representation of a tagged literal" + [o] + (instance? basilisp.lang.tagged/TaggedLiteral o)) + (defn ^:inline namespace "Return the namespace of a symbol or keyword, or ``nil`` if no namespace." [v] diff --git a/src/basilisp/lang/compiler/generator.py b/src/basilisp/lang/compiler/generator.py index daec389b5..ff3e235d8 100644 --- a/src/basilisp/lang/compiler/generator.py +++ b/src/basilisp/lang/compiler/generator.py @@ -698,6 +698,7 @@ def _var_ns_as_python_sym(name: str) -> str: _SEQ_ALIAS = genname("seq") _SET_ALIAS = genname("lset") _SYM_ALIAS = genname("sym") +_TAGGED_ALIAS = genname("tagged") _VEC_ALIAS = genname("vec") _VOLATILE_ALIAS = genname("volatile") _VAR_ALIAS = genname("Var") @@ -731,6 +732,7 @@ def _var_ns_as_python_sym(name: str) -> str: "basilisp.lang.seq": _SEQ_ALIAS, "basilisp.lang.set": _SET_ALIAS, "basilisp.lang.symbol": _SYM_ALIAS, + "basilisp.lang.tagged": _TAGGED_ALIAS, "basilisp.lang.vector": _VEC_ALIAS, "basilisp.lang.volatile": _VOLATILE_ALIAS, "basilisp.lang.util": _UTIL_ALIAS, diff --git a/src/basilisp/lang/runtime.py b/src/basilisp/lang/runtime.py index 59033717f..096135d05 100644 --- a/src/basilisp/lang/runtime.py +++ b/src/basilisp/lang/runtime.py @@ -551,6 +551,7 @@ class Namespace(ReferenceBase): "basilisp.lang.seq", "basilisp.lang.set", "basilisp.lang.symbol", + "basilisp.lang.tagged", "basilisp.lang.vector", "basilisp.lang.volatile", "basilisp.lang.util", diff --git a/src/basilisp/lang/tagged.py b/src/basilisp/lang/tagged.py new file mode 100644 index 000000000..6d38863a8 --- /dev/null +++ b/src/basilisp/lang/tagged.py @@ -0,0 +1,74 @@ +from typing import Optional, TypeVar, Union + +from typing_extensions import Unpack + +from basilisp.lang.interfaces import ILispObject, ILookup +from basilisp.lang.keyword import keyword +from basilisp.lang.obj import PrintSettings, lrepr +from basilisp.lang.symbol import Symbol + +K = TypeVar("K") +V = TypeVar("V") +T = Union[None, V, Symbol] + +_TAG_KW = keyword("tag") +_FORM_KW = keyword("form") + + +class TaggedLiteral( + ILispObject, + ILookup[K, T], +): + """Basilisp TaggedLiteral. https://clojure.org/reference/reader#tagged_literals""" + + __slots__ = ("_tag", "_form", "_hash") + + def __init__(self, tag: Symbol, form) -> None: + self._tag = tag + self._form = form + self._hash: Optional[int] = None + + @property + def tag(self) -> Symbol: + return self._tag + + @property + def form(self): + return self._form + + def __bool__(self): + return True + + def __eq__(self, other): + if self is other: + return True + if not isinstance(other, TaggedLiteral): + return NotImplemented + return self._tag == other._tag and self._form == other._form + + def __hash__(self): + if self._hash is None: + self._hash = hash((self._tag, self._form)) + return self._hash + + def __getitem__(self, item): + return self.val_at(item) + + def val_at(self, k: K, default: Optional[V] = None) -> T: + if k == _TAG_KW: + return self._tag + elif k == _FORM_KW: + return self._form + else: + return default + + def _lrepr(self, **kwargs: Unpack[PrintSettings]) -> str: + return f"#{self._tag} {lrepr(self._form, **kwargs)}" + + +def tagged_literal(tag: Symbol, form): + """Construct a data representation of a tagged literal from a + tag symbol and a form.""" + if not isinstance(tag, Symbol): + raise TypeError(f"tag must be a Symbol, not '{type(tag)}'") + return TaggedLiteral(tag, form) diff --git a/tests/basilisp/tagged_test.py b/tests/basilisp/tagged_test.py new file mode 100644 index 000000000..8d909a6f0 --- /dev/null +++ b/tests/basilisp/tagged_test.py @@ -0,0 +1,18 @@ +from basilisp.lang.symbol import symbol +from basilisp.lang.tagged import tagged_literal + + +def test_tagged_literal(): + tag = symbol("tag") + form = 1 + tagged = tagged_literal(tag, form) + assert tagged.tag == tag + assert tagged.form == form + + +def test_tagged_literal_str_and_repr(): + tag = symbol("tag") + form = 1 + tagged = tagged_literal(tag, form) + assert str(tagged) == "#tag 1" + assert repr(tagged) == "#tag 1" diff --git a/tests/basilisp/test_tagged.lpy b/tests/basilisp/test_tagged.lpy new file mode 100644 index 000000000..2c73361c9 --- /dev/null +++ b/tests/basilisp/test_tagged.lpy @@ -0,0 +1,33 @@ +(ns tests.basilisp.test-tagged + (:require + [basilisp.test :refer [deftest is testing]])) + +(deftest tagged-literal-test + (let [tag 'tag + form 1 + tagged (tagged-literal tag form)] + (testing "equality" + (is (= tagged tagged)) + (is (= tagged (tagged-literal tag form))) + (is (not= tagged (tagged-literal 'foo form))) + (is (not= tagged (tagged-literal tag 2)))) + + (testing "accessors" + (is (= tag (:tag tagged))) + (is (= form (:form tagged))) + (is (nil? (:key tagged))) + (is (= ::default (:key tagged ::default)))) + + (testing "predicate" + (is (true? (tagged-literal? tagged))) + (is (false? (tagged-literal? nil))) + (is (false? (tagged-literal? 0))) + (is (false? (tagged-literal? ::foo)))) + + (testing "printing" + (is (= "#tag 1" (pr-str tagged))) + (is (= "#js []" (pr-str (tagged-literal 'js [])))) + (is (= "#js {}" (pr-str (tagged-literal 'js {}))))) + + (testing "validation" + (is (thrown? TypeError (tagged-literal 1 1)))))) \ No newline at end of file