From cbb00fb43701ef7e78655b99b67b3a5a5dfec7ea Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Mon, 9 Dec 2024 09:56:56 -0500 Subject: [PATCH 1/2] Add support for pre- and post-conditions on fn forms --- CHANGELOG.md | 1 + src/basilisp/core.lpy | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 424ed166..bbfec162 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 optional `attr-map?` on the `ns` macro (#1159) + * Added support for the optional pre- and post-conditions in `fn` forms (#1167) ### Fixed * Fix a bug where `#` characters were not legal in keywords and symbols (#1149) diff --git a/src/basilisp/core.lpy b/src/basilisp/core.lpy index d8e34312..7684b22b 100644 --- a/src/basilisp/core.lpy +++ b/src/basilisp/core.lpy @@ -5940,6 +5940,25 @@ (let [args (first body) arg-vec-meta (meta args) body (rest body) + conditions (when (and (next body) (map? (first body))) + (first body)) + body (if conditions + (rest body) + body) + + conditions (or conditions arg-vec-meta) + pre-conds (:pre conditions) + post-conds (:post conditions) + body (if post-conds + [`(let [~'% (do ~@body)] + ~@(map (fn* [c] `(assert ~c)) post-conds) + ~'%)] + body) + body (if pre-conds + (concat + (map (fn* [c] `(assert ~c)) pre-conds) + body) + body) arg-groups (split-with (partial not= '&) args) args (first arg-groups) @@ -5964,6 +5983,7 @@ (filter #(not= :symbol (:type %))) (mapcat destructure-binding) (concat rest-binding)) + new-body (if (seq bindings) [`(let* [~@bindings] ~@body)] From 3bae58dd5ce3dd7f277a18a19153df70f0156293 Mon Sep 17 00:00:00 2001 From: Chris Rink Date: Mon, 9 Dec 2024 10:23:11 -0500 Subject: [PATCH 2/2] Tests --- tests/basilisp/test_core_macros.lpy | 98 +++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/basilisp/test_core_macros.lpy b/tests/basilisp/test_core_macros.lpy index 68685902..2926f1e6 100644 --- a/tests/basilisp/test_core_macros.lpy +++ b/tests/basilisp/test_core_macros.lpy @@ -1795,6 +1795,104 @@ (conj accum [a b c d e f v])) [coll accum]))))))) +(deftest fn-pre-and-post-condition-test + (testing "fn with map as body member does not count as condition map" + (let [f (fn [v] + {:pre [v] + :post [:nothin]})] + (is (= {:pre [1] :post [:nothin]} (f 1))))) + + (testing "pre-condition only" + (testing "empty pre-condition vec" + (let [f (fn [x] + {:pre []} + (* x x))] + (is (= 1 (f 1))) + (is (= 100 (f 10))) + (is (= 0 (f 0))) + (is (= 25 (f -5))))) + + (testing "single pre-condition" + (let [f (fn [x] + {:pre [(pos? x)]} + (* x x))] + (is (= 1 (f 1))) + (is (= 100 (f 10))) + (is (thrown? python/AssertionError + (f 0))) + (is (thrown? python/AssertionError + (f -5))))) + + (testing "multiple pre-condition" + (let [f (fn [x] + {:pre [(pos? x) (even? x)]} + (* x x))] + (is (= 4 (f 2))) + (is (= 100 (f 10))) + (is (thrown? python/AssertionError + (f 1))) + (is (thrown? python/AssertionError + (f 0))) + (is (thrown? python/AssertionError + (f -5))) + (is (thrown? python/AssertionError + (f -6)))))) + + (testing "post-condition only" + (testing "empty post-condition vec" + (let [f (fn [x] + {:post []} + (* x x))] + (is (= 1 (f 1))) + (is (= 100 (f 10))) + (is (= 0 (f 0))) + (is (= 25 (f -5))))) + + (testing "single post-condition" + (let [f (fn [x] + {:post [(> % 16)]} + (* x x))] + (is (= 25 (f 5))) + (is (= 100 (f 10))) + (is (thrown? python/AssertionError + (f 1))) + (is (thrown? python/AssertionError + (f -3))))) + + (testing "multiple post-condition" + (let [f (fn [x] + {:post [(> % 16) (< % 225)]} + (* x x))] + (is (= 25 (f 5))) + (is (= 25 (f -5))) + (is (= 196 (f 14))) + (is (thrown? python/AssertionError + (f 1))) + (is (thrown? python/AssertionError + (f 0))) + (is (thrown? python/AssertionError + (f 15))) + (is (thrown? python/AssertionError + (f 100)))))) + + (testing "pre- and post-conditions" + (let [f (fn [x] + {:pre [(pos? x)] + :post [(> % 16) (< % 225)]} + (* x x))] + (is (= 25 (f 5))) + (is (= 196 (f 14))) + (is (thrown? python/AssertionError + (f 1))) + (is (thrown? python/AssertionError + (f 0))) + (is (thrown? python/AssertionError + (f 15))) + (is (thrown? python/AssertionError + (f 100))) + (is (thrown? python/AssertionError + (f -5)))))) + (defmacro ^:private variadic-fn [] `(fn [& r#] r#))