From 58609803564f8ab7af32d5596c0447cfef3b8ab9 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Nov 2024 10:06:01 -0700 Subject: [PATCH 1/7] Add tagged-literal support --- src/basilisp/core.lpy | 11 ++++ src/basilisp/lang/compiler/generator.py | 2 + src/basilisp/lang/runtime.py | 1 + src/basilisp/lang/tagged.py | 85 +++++++++++++++++++++++++ tests/basilisp/tagged_test.py | 16 +++++ 5 files changed, 115 insertions(+) create mode 100644 src/basilisp/lang/tagged.py create mode 100644 tests/basilisp/tagged_test.py diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 8428226ab..b0572529e 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -547,6 +547,17 @@ v (.-name v))) +(defn 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 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..085bd3048 --- /dev/null +++ b/src/basilisp/lang/tagged.py @@ -0,0 +1,85 @@ +from typing import ( + Optional, + TypeVar, +) + +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") + +class TaggedLiteral( + ILispObject, + ILookup[K, V], +): + """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 = -1 + + @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 == -1: + self._hash = hash((self._tag, self._form)) + return self._hash + + def __getitem__(self, item): + if item == Keyword("tag"): + return self._tag + elif item == Keyword("form"): + return self._form + else: + return None + + def val_at(self, k: K, default: Optional[V] = None) -> Optional[V]: + if k == Keyword("tag"): + return self._tag + elif k == Keyword("form"): + 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..58ea2eaec --- /dev/null +++ b/tests/basilisp/tagged_test.py @@ -0,0 +1,16 @@ +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" From dae291db42ccd5c9607454aa60d69e9acecfe362 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Nov 2024 10:11:08 -0700 Subject: [PATCH 2/7] Add more tests --- tests/basilisp/test_tagged.lpy | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/basilisp/test_tagged.lpy diff --git a/tests/basilisp/test_tagged.lpy b/tests/basilisp/test_tagged.lpy new file mode 100644 index 000000000..bfb421a19 --- /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 From 801ee73d08c9285ea020b91b2a192fd01f686b00 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Thu, 7 Nov 2024 18:39:48 -0700 Subject: [PATCH 3/7] Address PR comments --- src/basilisp/core.lpy | 4 ++-- src/basilisp/lang/tagged.py | 26 +++++++++++++------------- tests/basilisp/test_tagged.lpy | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index b0572529e..101ea2382 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -547,13 +547,13 @@ v (.-name v))) -(defn tagged-literal +(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 tagged-literal? +(defn ^:inline tagged-literal? "Return true if the value is the data representation of a tagged literal" [o] (instance? basilisp.lang.tagged/TaggedLiteral o)) diff --git a/src/basilisp/lang/tagged.py b/src/basilisp/lang/tagged.py index 085bd3048..80a46bde7 100644 --- a/src/basilisp/lang/tagged.py +++ b/src/basilisp/lang/tagged.py @@ -1,6 +1,7 @@ from typing import ( Optional, TypeVar, + Union, ) from typing_extensions import Unpack @@ -10,16 +11,20 @@ ILookup, ) -from basilisp.lang.keyword import Keyword +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, V], + ILookup[K, T], ): """Basilisp TaggedLiteral. https://clojure.org/reference/reader#tagged_literals """ @@ -31,7 +36,7 @@ def __init__( ) -> None: self._tag = tag self._form = form - self._hash = -1 + self._hash : Union[None, int] = None @property def tag(self) -> Symbol: @@ -52,22 +57,17 @@ def __eq__(self, other): return self._tag == other._tag and self._form == other._form def __hash__(self): - if self._hash == -1: + if self._hash == None: self._hash = hash((self._tag, self._form)) return self._hash def __getitem__(self, item): - if item == Keyword("tag"): - return self._tag - elif item == Keyword("form"): - return self._form - else: - return None + return self.val_at(item) - def val_at(self, k: K, default: Optional[V] = None) -> Optional[V]: - if k == Keyword("tag"): + def val_at(self, k: K, default: Optional[V] = None) -> T: + if k == _TAG_KW: return self._tag - elif k == Keyword("form"): + elif k == _FORM_KW: return self._form else: return default diff --git a/tests/basilisp/test_tagged.lpy b/tests/basilisp/test_tagged.lpy index bfb421a19..2c73361c9 100644 --- a/tests/basilisp/test_tagged.lpy +++ b/tests/basilisp/test_tagged.lpy @@ -30,4 +30,4 @@ (is (= "#js {}" (pr-str (tagged-literal 'js {}))))) (testing "validation" - (is (thrown? TypeError (tagged-literal 1 1)) )))) \ No newline at end of file + (is (thrown? TypeError (tagged-literal 1 1)))))) \ No newline at end of file From ba0f1525f0c6843bd7b1eed7d866a64957b212f7 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Thu, 7 Nov 2024 18:49:22 -0700 Subject: [PATCH 4/7] Update src/basilisp/lang/tagged.py Co-authored-by: Chris Rink --- src/basilisp/lang/tagged.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basilisp/lang/tagged.py b/src/basilisp/lang/tagged.py index 80a46bde7..1d48c9d8d 100644 --- a/src/basilisp/lang/tagged.py +++ b/src/basilisp/lang/tagged.py @@ -57,7 +57,7 @@ def __eq__(self, other): return self._tag == other._tag and self._form == other._form def __hash__(self): - if self._hash == None: + if self._hash is None: self._hash = hash((self._tag, self._form)) return self._hash From b8f59a888845a39ef12a7e71f85349dd237f0016 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Thu, 7 Nov 2024 18:49:27 -0700 Subject: [PATCH 5/7] Update src/basilisp/lang/tagged.py Co-authored-by: Chris Rink --- src/basilisp/lang/tagged.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basilisp/lang/tagged.py b/src/basilisp/lang/tagged.py index 1d48c9d8d..b64503f69 100644 --- a/src/basilisp/lang/tagged.py +++ b/src/basilisp/lang/tagged.py @@ -36,7 +36,7 @@ def __init__( ) -> None: self._tag = tag self._form = form - self._hash : Union[None, int] = None + self._hash : Optional[int] = None @property def tag(self) -> Symbol: From 41b4ec068dba9d1e9998bc2b32a38b4daa4d2025 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Thu, 7 Nov 2024 18:55:34 -0700 Subject: [PATCH 6/7] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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) From a75d4ac2752413e289e780bc672cbe64db4d3a3b Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Thu, 7 Nov 2024 19:18:10 -0700 Subject: [PATCH 7/7] Run formatter --- src/basilisp/lang/tagged.py | 27 ++++++++------------------- tests/basilisp/tagged_test.py | 2 ++ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/basilisp/lang/tagged.py b/src/basilisp/lang/tagged.py index b64503f69..6d38863a8 100644 --- a/src/basilisp/lang/tagged.py +++ b/src/basilisp/lang/tagged.py @@ -1,16 +1,8 @@ -from typing import ( - Optional, - TypeVar, - Union, -) +from typing import Optional, TypeVar, Union from typing_extensions import Unpack -from basilisp.lang.interfaces import ( - ILispObject, - ILookup, -) - +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 @@ -22,21 +14,19 @@ _TAG_KW = keyword("tag") _FORM_KW = keyword("form") + class TaggedLiteral( ILispObject, ILookup[K, T], ): - """Basilisp TaggedLiteral. https://clojure.org/reference/reader#tagged_literals - """ + """Basilisp TaggedLiteral. https://clojure.org/reference/reader#tagged_literals""" __slots__ = ("_tag", "_form", "_hash") - def __init__( - self, tag: Symbol, form - ) -> None: + def __init__(self, tag: Symbol, form) -> None: self._tag = tag self._form = form - self._hash : Optional[int] = None + self._hash: Optional[int] = None @property def tag(self) -> Symbol: @@ -75,9 +65,8 @@ def val_at(self, k: K, default: Optional[V] = None) -> T: def _lrepr(self, **kwargs: Unpack[PrintSettings]) -> str: return f"#{self._tag} {lrepr(self._form, **kwargs)}" -def tagged_literal( - tag: Symbol, form -): + +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): diff --git a/tests/basilisp/tagged_test.py b/tests/basilisp/tagged_test.py index 58ea2eaec..8d909a6f0 100644 --- a/tests/basilisp/tagged_test.py +++ b/tests/basilisp/tagged_test.py @@ -1,6 +1,7 @@ from basilisp.lang.symbol import symbol from basilisp.lang.tagged import tagged_literal + def test_tagged_literal(): tag = symbol("tag") form = 1 @@ -8,6 +9,7 @@ def test_tagged_literal(): assert tagged.tag == tag assert tagged.form == form + def test_tagged_literal_str_and_repr(): tag = symbol("tag") form = 1