diff --git a/CHANGELOG.md b/CHANGELOG.md index 31f8ecf46..60a091b32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + * Added support for constructing a data representation of reader conditionals (#1125) + ### Fixed * Fix a bug where `basilisp test` command fails due to an invalid `argparse` configuration (#1119) * Fix a bug where `basilisp.walk/walk` (and any functions that depend on it) did not preserve collection metadata (#1123) diff --git a/docs/reader.rst b/docs/reader.rst index 5f46cce36..96fc32bbd 100644 --- a/docs/reader.rst +++ b/docs/reader.rst @@ -537,7 +537,7 @@ In nearly all cases, this will be the return value from a macro function, which :ref:`macros` -.. _reader_conditions: +.. _reader_conditionals: Reader Conditionals ------------------- @@ -572,6 +572,10 @@ Splicing reader conditionals may only appear within other collection literal for basilisp.user=> #?@(:lpy [1 2 3]) basilisp.lang.reader.SyntaxError: Unexpected reader conditional +.. seealso:: + + :lpy:fn:`reader-conditional`, :lpy:fn:`reader-conditional?` + .. _python_version_reader_features: Python Version Reader Features diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index 101ea2382..19c29a003 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -547,14 +547,25 @@ v (.-name v))) +(defn ^:inline reader-conditional + "Construct a data representation of a :ref:`reader conditional `. + + The form must contain balanced key-value pairs." + [form is-splicing?] + (basilisp.lang.reader/ReaderConditional form is-splicing?)) + +(defn ^:inline reader-conditional? + "Return true if the value is the data representation of a :ref:`reader conditional `." + [o] + (instance? basilisp.lang.reader/ReaderConditional o)) + (defn ^:inline tagged-literal - "Construct a data representation of a tagged literal from a - tag symbol and a form." + "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" + "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/reader.py b/src/basilisp/lang/reader.py index db6793893..658cd4088 100644 --- a/src/basilisp/lang/reader.py +++ b/src/basilisp/lang/reader.py @@ -72,7 +72,8 @@ fn_macro_args = re.compile("(%)(&|[0-9])?") unicode_char = re.compile(r"u(\w+)") -DataReaders = Optional[lmap.PersistentMap] +DataReaderFn = Callable[[Any], Any] +DataReaders = lmap.PersistentMap[sym.Symbol, DataReaderFn] GenSymEnvironment = MutableMapping[str, sym.Symbol] Resolver = Callable[[sym.Symbol], sym.Symbol] LispReaderFn = Callable[["ReaderContext"], LispForm] @@ -348,7 +349,7 @@ def _raise_unknown_tag(s: sym.Symbol, v: LispReaderForm) -> NoReturn: class ReaderContext: - _DATA_READERS = lmap.map( + _DATA_READERS: DataReaders = lmap.map( { sym.symbol("inst"): _inst_from_str, sym.symbol("py"): _py_from_lisp, @@ -394,7 +395,7 @@ def __init__( # pylint: disable=too-many-arguments self._eof = eof @property - def data_readers(self) -> lmap.PersistentMap: + def data_readers(self) -> DataReaders: return self._data_readers @property @@ -487,6 +488,13 @@ def __init__( self._feature_vec = self._compile_feature_vec(form) self._is_splicing = is_splicing + def __eq__(self, other): + if self is other: + return True + if not isinstance(other, ReaderConditional): + return NotImplemented + return self._form == other._form and self._is_splicing == other._is_splicing + @staticmethod def _compile_feature_vec(form: IPersistentList[tuple[kw.Keyword, ReaderForm]]): found_features: set[kw.Keyword] = set() diff --git a/src/basilisp/lang/runtime.py b/src/basilisp/lang/runtime.py index 096135d05..f89672c07 100644 --- a/src/basilisp/lang/runtime.py +++ b/src/basilisp/lang/runtime.py @@ -2434,7 +2434,7 @@ def in_ns(s: sym.Symbol): { _DOC_META_KEY: ( "The set of all currently supported " - ":ref:`reader features `." + ":ref:`reader features `." ) } ), diff --git a/src/basilisp/lang/tagged.py b/src/basilisp/lang/tagged.py index 6d38863a8..9237368e3 100644 --- a/src/basilisp/lang/tagged.py +++ b/src/basilisp/lang/tagged.py @@ -15,10 +15,7 @@ _FORM_KW = keyword("form") -class TaggedLiteral( - ILispObject, - ILookup[K, T], -): +class TaggedLiteral(ILispObject, ILookup[K, T]): """Basilisp TaggedLiteral. https://clojure.org/reference/reader#tagged_literals""" __slots__ = ("_tag", "_form", "_hash") diff --git a/tests/basilisp/test_core_fns.lpy b/tests/basilisp/test_core_fns.lpy index 289beb4fc..9536e0aa8 100644 --- a/tests/basilisp/test_core_fns.lpy +++ b/tests/basilisp/test_core_fns.lpy @@ -461,8 +461,6 @@ (is (= 1 (rand-nth [1]))) (is (#{1 2} (rand-nth [1 2])))) - - (deftest subvec-test (is (= [] (subvec [] 0))) (is (thrown? python/IndexError (subvec [] 3))) diff --git a/tests/basilisp/test_core_reader_utility_fns.lpy b/tests/basilisp/test_core_reader_utility_fns.lpy new file mode 100644 index 000000000..987da4861 --- /dev/null +++ b/tests/basilisp/test_core_reader_utility_fns.lpy @@ -0,0 +1,68 @@ +(ns tests.basilisp.test-core-reader-utility-fns + (:require + [basilisp.test :refer [deftest is testing]])) + +(deftest reader-conditional-test + (let [form '() + is-splicing? true + reader-cond (reader-conditional form is-splicing?)] + (testing "equality" + (is (= reader-cond reader-cond)) + (is (= reader-cond (reader-conditional form is-splicing?))) + (is (not= reader-cond (reader-conditional '(:clj [] :lpy [true]) is-splicing?))) + (is (not= reader-cond (reader-conditional form false)))) + + (testing "accessors" + (is (= form (:form reader-cond))) + (is (= is-splicing? (:splicing? reader-cond))) + (is (nil? (:key reader-cond))) + (is (= ::default (:key reader-cond ::default)))) + + (testing "predicate" + (is (true? (reader-conditional? reader-cond))) + (is (false? (reader-conditional? nil))) + (is (false? (reader-conditional? 0))) + (is (false? (reader-conditional? ::foo)))) + + (testing "printing" + (is (= "#?@()" (pr-str reader-cond))) + (is (= "#?()" (pr-str (reader-conditional '() false)))) + (is (= "#?@(:clj [] :lpy [true])" (pr-str (reader-conditional '(:clj [] :lpy [true]) true))))) + + (testing "validation" + (is (thrown? basilisp.lang.reader/SyntaxError + (reader-conditional '(:clj) true))) + (is (thrown? basilisp.lang.reader/SyntaxError + (reader-conditional '(:clj [] :lpy) true))) + (is (thrown? basilisp.lang.reader/SyntaxError + (reader-conditional '('lpy [] :clj [true]) true)))))) + +(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)))))) diff --git a/tests/basilisp/test_tagged.lpy b/tests/basilisp/test_tagged.lpy deleted file mode 100644 index 2c73361c9..000000000 --- a/tests/basilisp/test_tagged.lpy +++ /dev/null @@ -1,33 +0,0 @@ -(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