From b4e1c596c0c84971f7b42999933b0ad820432a04 Mon Sep 17 00:00:00 2001 From: Sophia Velten de Melo Date: Thu, 29 May 2025 15:31:13 -0300 Subject: [PATCH 01/23] update node logic --- project.clj | 2 +- src/nodely/data.clj | 36 +++++++++++++++++++++++++++++++ src/nodely/engine/applicative.clj | 7 ++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 984ca87..76def45 100644 --- a/project.clj +++ b/project.clj @@ -6,7 +6,7 @@ :plugins [[s3-wagon-private "1.3.4" :exclusions [com.fasterxml.jackson.core/jackson-core]] [com.fasterxml.jackson.core/jackson-core "2.12.4"]] - :dependencies [[org.clojure/clojure "1.10.3"] + :dependencies [[org.clojure/clojure "1.12.0"] [aysylu/loom "1.0.2"] [org.clojure/core.async "1.5.648" :scope "provided"] [funcool/promesa "10.0.594" :scope "provided"] diff --git a/src/nodely/data.clj b/src/nodely/data.clj index f92f8ef..0df46d1 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -116,10 +116,46 @@ :sequence (recur (::process-node node) (conj inputs (::input node)))))) +(defn update-leaf + [leaf f] + (update leaf :nodely.data/fn #(comp f %))) + +(declare update-node) + +(defn update-branch + [{::keys [condition truthy falsey]} + f + {:keys [apply-to-condition?] + :or {apply-to-condition? false}}] + #::{:type :branch + :condition (if apply-to-condition? + (update-node condition f) + condition) + :falsey (update-node falsey f) + :truthy (update-node truthy f)}) + +(defn update-sequence + [sequence f] + (update sequence ::process-node update-node f)) + +(defn update-node + ([node f opts] + (case (::type node) + :value (update node ::value f) + :leaf (update-leaf node f) + :branch (update-branch node f opts) + :sequence (update-sequence node f))) + ([node f] + (update-node node f {}))) + ;; ;; Env Utils ;; +(defn with-error-handler + [env handler] + (update-vals env #(update-node % handler {:apply-to-condition? true}))) + (s/defn get-value :- s/Any [env :- Env k :- s/Keyword] diff --git a/src/nodely/engine/applicative.clj b/src/nodely/engine/applicative.clj index dbf6726..e7acaa1 100644 --- a/src/nodely/engine/applicative.clj +++ b/src/nodely/engine/applicative.clj @@ -98,3 +98,10 @@ (reduce (fn [acc [k v]] (assoc acc k (protocols/-extract v))) {} (lazy-env/scheduled-nodes lazy-env))))) + +(comment + + {:a (>leaf (throw (Exception. "My exception"))) + :b (>leaf 2) + :c (>leaf (+ ?a ?b))} + ) From cb15457d92cc18fc8b2dc153891b9ecb0fb91aa3 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Thu, 12 Jun 2025 15:29:36 -0300 Subject: [PATCH 02/23] add generated llm claude-4-sonnet tests --- test/nodely/data_test.clj | 127 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/test/nodely/data_test.clj b/test/nodely/data_test.clj index d631769..2630166 100644 --- a/test/nodely/data_test.clj +++ b/test/nodely/data_test.clj @@ -2,6 +2,7 @@ (:refer-clojure :exclude [cond]) (:require [clojure.test :refer :all] + [clojure.string :as string] [nodely.data :as data])) (deftest node? @@ -26,3 +27,129 @@ (data/leaf [:a] identity)) (data/leaf [:b] identity) (data/leaf [:c] identity))))))) + +(deftest update-leaf-test + (testing "update-leaf modifies the function by composing with the given function" + (let [original-leaf (data/leaf [:x] (comp inc :x)) + updated-leaf (data/update-leaf original-leaf (partial * 2))] + (is (= 12 ((::data/fn updated-leaf) {:x 5}))))) + + (testing "update-leaf preserves other leaf properties" + (let [original-leaf (data/leaf [:a :b] (fn [{:keys [a b]}] (str a " " b)) #{::data/blocking}) + updated-leaf (data/update-leaf original-leaf (fn [s] (str s "!")))] + (is (= (::data/type updated-leaf) :leaf)) + (is (= (::data/inputs updated-leaf) #{:a :b})) + (is (= (::data/tags updated-leaf) #{::data/blocking})) + ;; Test function composition: (fn [s] (str s "!")) ∘ (fn [{:keys [a b]}] (str a " " b)) + (is (= "hello world!" ((::data/fn updated-leaf) {:a "hello" :b "world"})))))) + +(deftest update-branch-test + (testing "update-branch without apply-to-condition updates truthy and falsey branches" + (let [condition (data/leaf [:x] (comp even? :x)) + truthy (data/leaf [:y] (comp inc :y)) + falsey (data/leaf [:z] (comp dec :z)) + branch (data/branch condition truthy falsey) + updated-branch (data/update-branch branch (partial * 2) {})] + (is (= (::data/type updated-branch) :branch)) + ;; Condition should remain unchanged + (is (= (::data/condition updated-branch) condition)) + ;; Truthy and falsey branches should be updated + (is (= 20 ((::data/fn (::data/truthy updated-branch)) {:y 9}))) ; (* 2 (inc 9)) = 20 + (is (= 18 ((::data/fn (::data/falsey updated-branch)) {:z 10}))))) ; (* 2 (dec 10)) = 18 + + (testing "update-branch with apply-to-condition updates all branches including condition" + (let [condition (data/leaf [:x] (comp inc :x)) + truthy (data/leaf [:y] (comp inc :y)) + falsey (data/leaf [:z] (comp inc :z)) + branch (data/branch condition truthy falsey) + updated-branch (data/update-branch branch (partial * 2) {:apply-to-condition? true})] + (is (= (::data/type updated-branch) :branch)) + ;; All branches should be updated + (is (= 12 ((::data/fn (::data/condition updated-branch)) {:x 5}))) ; (* 2 (inc 5)) = 12 + (is (= 12 ((::data/fn (::data/truthy updated-branch)) {:y 5}))) ; (* 2 (inc 5)) = 12 + (is (= 12 ((::data/fn (::data/falsey updated-branch)) {:z 5}))))) ; (* 2 (inc 5)) = 12 + + (testing "update-branch with value nodes in truthy/falsey branches" + (let [condition (data/leaf [:x] (comp even? :x)) + truthy (data/value 42) + falsey (data/value 100) + branch (data/branch condition truthy falsey) + updated-branch (data/update-branch branch (partial * 2) {})] + (is (= (::data/type updated-branch) :branch)) + (is (= (::data/condition updated-branch) condition)) + ;; Value nodes should be updated by applying the function to their values + (is (= 84 (::data/value (::data/truthy updated-branch)))) ; (* 2 42) = 84 + (is (= 200 (::data/value (::data/falsey updated-branch))))))) ; (* 2 100) = 200 + +(deftest update-sequence-test + (testing "update-sequence with value process-node" + (let [sequence-node (data/sequence :items 10) + updated-sequence (data/update-sequence sequence-node (partial * 2))] + (is (= (::data/type updated-sequence) :sequence)) + (is (= (::data/input updated-sequence) :items)) + (is (= (::data/type (::data/process-node updated-sequence)) :value)) + ;; The process-node value should be updated + (is (= 20 (::data/value (::data/process-node updated-sequence)))))) ; (* 2 10) = 20 + + (testing "update-sequence with leaf process-node" + (let [sequence-node (data/sequence :items [:closure-var] (comp inc :closure-var) #{}) + updated-sequence (data/update-sequence sequence-node (partial * 2))] + (is (= (::data/type updated-sequence) :sequence)) + (is (= (::data/input updated-sequence) :items)) + (is (= (::data/type (::data/process-node updated-sequence)) :leaf)) + (is (= (::data/inputs (::data/process-node updated-sequence)) #{:closure-var})) + ;; The leaf function should be updated + (is (= 12 ((::data/fn (::data/process-node updated-sequence)) {:closure-var 5})))))) ; (* 2 (inc 5)) = 12 + +(deftest update-node-test + (testing "update-node with value node" + (let [value-node (data/value 42) + updated-node (data/update-node value-node (partial * 2))] + (is (= (::data/type updated-node) :value)) + (is (= 84 (::data/value updated-node))))) ; (* 2 42) = 84 + + (testing "update-node with leaf node" + (let [leaf-node (data/leaf [:x] (comp inc :x)) + updated-node (data/update-node leaf-node (partial * 2))] + (is (= (::data/type updated-node) :leaf)) + (is (= (::data/inputs updated-node) #{:x})) + (is (= 12 ((::data/fn updated-node) {:x 5}))))) ; (* 2 (inc 5)) = 12 + + (testing "update-node with branch node" + (let [condition (data/leaf [:x] (comp even? :x)) + truthy (data/value 10) + falsey (data/value 20) + branch-node (data/branch condition truthy falsey) + updated-node (data/update-node branch-node (partial * 2))] + (is (= (::data/type updated-node) :branch)) + ;; Condition should not be updated by default + (is (= (::data/condition updated-node) condition)) + ;; Truthy and falsey should be updated + (is (= 20 (::data/value (::data/truthy updated-node)))) ; (* 2 10) = 20 + (is (= 40 (::data/value (::data/falsey updated-node)))))) ; (* 2 20) = 40 + + (testing "update-node with branch node and apply-to-condition option" + (let [condition (data/leaf [:x] (comp inc :x)) + truthy (data/value 10) + falsey (data/value 20) + branch-node (data/branch condition truthy falsey) + updated-node (data/update-node branch-node (partial * 2) {:apply-to-condition? true})] + (is (= (::data/type updated-node) :branch)) + ;; All parts should be updated + (is (= 12 ((::data/fn (::data/condition updated-node)) {:x 5}))) ; (* 2 (inc 5)) = 12 + (is (= 20 (::data/value (::data/truthy updated-node)))) ; (* 2 10) = 20 + (is (= 40 (::data/value (::data/falsey updated-node)))))) ; (* 2 20) = 40 + + (testing "update-node with sequence node" + (let [sequence-node (data/sequence :items 5) + updated-node (data/update-node sequence-node (partial * 2))] + (is (= (::data/type updated-node) :sequence)) + (is (= (::data/input updated-node) :items)) + (is (= 10 (::data/value (::data/process-node updated-node)))))) ; (* 2 5) = 10 + + (testing "update-node with 2-arity (no options)" + (let [leaf-node (data/leaf [:x] (comp inc :x)) + updated-node (data/update-node leaf-node (partial * 2))] + (is (= (::data/type updated-node) :leaf)) + (is (= (::data/inputs updated-node) #{:x})) + (is (= 12 ((::data/fn updated-node) {:x 5})))))) From 1346cbea51d93ed3bbd68c66db9f73d2973e1e12 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Thu, 26 Jun 2025 15:38:30 -0300 Subject: [PATCH 03/23] review generated llm claude-4-sonnet tests --- test/nodely/data_test.clj | 75 ++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 45 deletions(-) diff --git a/test/nodely/data_test.clj b/test/nodely/data_test.clj index 2630166..19787bd 100644 --- a/test/nodely/data_test.clj +++ b/test/nodely/data_test.clj @@ -2,7 +2,6 @@ (:refer-clojure :exclude [cond]) (:require [clojure.test :refer :all] - [clojure.string :as string] [nodely.data :as data])) (deftest node? @@ -31,17 +30,13 @@ (deftest update-leaf-test (testing "update-leaf modifies the function by composing with the given function" (let [original-leaf (data/leaf [:x] (comp inc :x)) - updated-leaf (data/update-leaf original-leaf (partial * 2))] - (is (= 12 ((::data/fn updated-leaf) {:x 5}))))) - - (testing "update-leaf preserves other leaf properties" - (let [original-leaf (data/leaf [:a :b] (fn [{:keys [a b]}] (str a " " b)) #{::data/blocking}) - updated-leaf (data/update-leaf original-leaf (fn [s] (str s "!")))] - (is (= (::data/type updated-leaf) :leaf)) - (is (= (::data/inputs updated-leaf) #{:a :b})) - (is (= (::data/tags updated-leaf) #{::data/blocking})) - ;; Test function composition: (fn [s] (str s "!")) ∘ (fn [{:keys [a b]}] (str a " " b)) - (is (= "hello world!" ((::data/fn updated-leaf) {:a "hello" :b "world"})))))) + updated-leaf (data/update-leaf original-leaf (partial * 2)) + expected-props {::data/type :leaf + ::data/inputs #{:x}}] + (is (= expected-props (select-keys updated-leaf (keys expected-props))) + "update-leaf should preserve other leaf properties") + (is (= 12 ((::data/fn updated-leaf) {:x 5})) + "update-leaf should modify the function by composing with the given function")))) (deftest update-branch-test (testing "update-branch without apply-to-condition updates truthy and falsey branches" @@ -50,10 +45,7 @@ falsey (data/leaf [:z] (comp dec :z)) branch (data/branch condition truthy falsey) updated-branch (data/update-branch branch (partial * 2) {})] - (is (= (::data/type updated-branch) :branch)) - ;; Condition should remain unchanged - (is (= (::data/condition updated-branch) condition)) - ;; Truthy and falsey branches should be updated + (is (= true ((::data/fn (::data/condition updated-branch)) {:x 2}))) ; (even? 2) = true (is (= 20 ((::data/fn (::data/truthy updated-branch)) {:y 9}))) ; (* 2 (inc 9)) = 20 (is (= 18 ((::data/fn (::data/falsey updated-branch)) {:z 10}))))) ; (* 2 (dec 10)) = 18 @@ -62,43 +54,36 @@ truthy (data/leaf [:y] (comp inc :y)) falsey (data/leaf [:z] (comp inc :z)) branch (data/branch condition truthy falsey) - updated-branch (data/update-branch branch (partial * 2) {:apply-to-condition? true})] - (is (= (::data/type updated-branch) :branch)) - ;; All branches should be updated + updated-branch (data/update-branch branch (partial * 2) {:apply-to-condition? true})] (is (= 12 ((::data/fn (::data/condition updated-branch)) {:x 5}))) ; (* 2 (inc 5)) = 12 (is (= 12 ((::data/fn (::data/truthy updated-branch)) {:y 5}))) ; (* 2 (inc 5)) = 12 (is (= 12 ((::data/fn (::data/falsey updated-branch)) {:z 5}))))) ; (* 2 (inc 5)) = 12 - (testing "update-branch with value nodes in truthy/falsey branches" - (let [condition (data/leaf [:x] (comp even? :x)) - truthy (data/value 42) - falsey (data/value 100) - branch (data/branch condition truthy falsey) - updated-branch (data/update-branch branch (partial * 2) {})] - (is (= (::data/type updated-branch) :branch)) - (is (= (::data/condition updated-branch) condition)) - ;; Value nodes should be updated by applying the function to their values - (is (= 84 (::data/value (::data/truthy updated-branch)))) ; (* 2 42) = 84 - (is (= 200 (::data/value (::data/falsey updated-branch))))))) ; (* 2 100) = 200 + (testing "update-branch with nested branches updates recursively" + (let [inner-condition (data/leaf [:a] (comp even? :a)) + inner-truthy (data/leaf [:b] (comp inc :b)) + inner-falsey (data/leaf [:c] (comp dec :c)) + inner-branch (data/branch inner-condition inner-truthy inner-falsey) + outer-condition (data/leaf [:x] (comp even? :x)) + outer-truthy inner-branch + outer-falsey (data/leaf [:z] (comp dec :z)) + outer-branch (data/branch outer-condition outer-truthy outer-falsey) + updated-branch (data/update-branch outer-branch (partial * 2) {})] + ;; Outer condition should not be updated by default + (is (= true ((::data/fn (::data/condition updated-branch)) {:x 2}))) ; (even? 2) = true + ;; Inner condition should not be updated by default + (is (= true ((::data/fn (::data/condition (::data/truthy updated-branch))) {:a 2}))) ; (even? 2) = true + ;; Inner truthy should be updated + (is (= 12 ((::data/fn (::data/truthy (::data/truthy updated-branch))) {:b 5}))) ; (* 2 (inc 5)) = 12 + ;; Inner falsey should be updated + (is (= 8 ((::data/fn (::data/falsey (::data/truthy updated-branch))) {:c 5}))) ; (* 2 (dec 5)) = 8 + ;; Outer falsey should be updated + (is (= 8 ((::data/fn (::data/falsey updated-branch)) {:z 5})))))) ; (* 2 (dec 5)) = 8 (deftest update-sequence-test - (testing "update-sequence with value process-node" - (let [sequence-node (data/sequence :items 10) - updated-sequence (data/update-sequence sequence-node (partial * 2))] - (is (= (::data/type updated-sequence) :sequence)) - (is (= (::data/input updated-sequence) :items)) - (is (= (::data/type (::data/process-node updated-sequence)) :value)) - ;; The process-node value should be updated - (is (= 20 (::data/value (::data/process-node updated-sequence)))))) ; (* 2 10) = 20 - - (testing "update-sequence with leaf process-node" + (testing "update-sequence composes the function argument" (let [sequence-node (data/sequence :items [:closure-var] (comp inc :closure-var) #{}) updated-sequence (data/update-sequence sequence-node (partial * 2))] - (is (= (::data/type updated-sequence) :sequence)) - (is (= (::data/input updated-sequence) :items)) - (is (= (::data/type (::data/process-node updated-sequence)) :leaf)) - (is (= (::data/inputs (::data/process-node updated-sequence)) #{:closure-var})) - ;; The leaf function should be updated (is (= 12 ((::data/fn (::data/process-node updated-sequence)) {:closure-var 5})))))) ; (* 2 (inc 5)) = 12 (deftest update-node-test From d8180dc411fb83e642fe78e9a2f1b233e13f407a Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Thu, 3 Jul 2025 15:31:50 -0300 Subject: [PATCH 04/23] add generated llm tests on api-test namespace --- src/nodely/api/v0.clj | 1 + test/nodely/api_test.clj | 58 ++++++++++++++++++++++++++++++++++++++- test/nodely/data_test.clj | 10 +++---- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/nodely/api/v0.clj b/src/nodely/api/v0.clj index 90c6c05..15e1d02 100644 --- a/src/nodely/api/v0.clj +++ b/src/nodely/api/v0.clj @@ -25,6 +25,7 @@ (import-fn nodely.engine.lazy/eval-node-with-values eval-node-with-values) (import-fn nodely.data/merge-values merge-values) (import-fn nodely.data/get-value get-value) +(import-fn nodely.data/update-node update-node) (def virtual-future-failure (delay diff --git a/test/nodely/api_test.clj b/test/nodely/api_test.clj index ccc50de..6bfb810 100644 --- a/test/nodely/api_test.clj +++ b/test/nodely/api_test.clj @@ -6,7 +6,7 @@ [clojure.test :refer :all] [criterium.core :as criterium] [matcher-combinators.matchers :as matchers] - [nodely.api.v0 :as api :refer [>leaf >sequence >value blocking]] + [nodely.api.v0 :as api :refer [>leaf >sequence >value blocking >if]] [nodely.test-helpers :as t])) (def env {:x (>value 2) @@ -246,3 +246,59 @@ :supported-engine-names set?} (try (api/eval-node-channel env (>leaf (inc ?z)) {::api/engine :core.async.doesnt-exist}) (catch clojure.lang.ExceptionInfo e (ex-data e))))))) + +(defn update-node-engine-test-suite + [engine-key] + (t/testing (name engine-key) + (t/testing "update-node functionality" + (t/testing "update-node and then eval a key that is affected by the update-node" + (let [original-env {:x (>value 2) + :y (>value 3) + :z (>leaf (+ ?x ?y))} + updated-env (update original-env :z api/update-node (partial * 2))] + (t/matching 10 (api/eval-key updated-env :z {::api/engine engine-key})))) + + (t/testing "update-node and then eval a key that is not affected by the update-node" + (let [original-env {:x (>value 2) + :y (>value 3) + :z (>leaf (+ ?x ?y))} + updated-env (update original-env :z api/update-node (partial * 2))] + (t/matching 2 (api/eval-key updated-env :x {::api/engine engine-key})) + (t/matching 3 (api/eval-key updated-env :y {::api/engine engine-key})))) + + (t/testing "update-node with value node" + (let [original-env {:x (>value 5) + :y (>leaf (* ?x 2))} + updated-env (update original-env :x api/update-node (partial + 3))] + (t/matching 8 (api/eval-key updated-env :x {::api/engine engine-key})) + (t/matching 16 (api/eval-key updated-env :y {::api/engine engine-key})))) + + (t/testing "update-node with sequence node" + (let [original-env {:x (>value [1 2 3]) + :y (>sequence inc ?x)} + updated-env (update original-env :y api/update-node (fn [f] (comp (partial * 2) f)))] + (t/matching [4 6 8] (api/eval-key updated-env :y {::api/engine engine-key})))) + + (t/testing "update-node with branch node" + (let [original-env {:x (>value 5) + :y (>if (>leaf (even? ?x)) (>value "even") (>value "odd"))} + updated-env (update original-env :y api/update-node (partial str "result: "))] + (t/matching "result: odd" (api/eval-key updated-env :y {::api/engine engine-key})))) + + (t/testing "update-node with apply-to-condition option" + (let [original-env {:x (>value 0) + :y (>if (>leaf ?x) (>value -10) (>value 20))} + updated-env (update original-env :y api/update-node pos? {:apply-to-condition? true})] + ;; original: ?x = 0 is truthy, so truthy branch => -10 + ;; after update: (pos? ?x) => (pos? 0) => false, so takes falsey branch => (pos? 20) => true + (t/matching true (api/eval-key updated-env :y {::api/engine engine-key}))))))) + +(t/deftest update-node-test + (let [remove-keys (conj #{:core-async.iterative-scheduling + :async.virtual-futures} + (when (try (import java.util.concurrent.ThreadPerTaskExecutor) + (catch Throwable t t)) + :applicative.virtual-future))] + (for [engine (set/difference (set (keys api/engine-data)) + remove-keys)] + (update-node-engine-test-suite engine)))) \ No newline at end of file diff --git a/test/nodely/data_test.clj b/test/nodely/data_test.clj index 19787bd..9701149 100644 --- a/test/nodely/data_test.clj +++ b/test/nodely/data_test.clj @@ -33,10 +33,10 @@ updated-leaf (data/update-leaf original-leaf (partial * 2)) expected-props {::data/type :leaf ::data/inputs #{:x}}] - (is (= expected-props (select-keys updated-leaf (keys expected-props))) - "update-leaf should preserve other leaf properties") - (is (= 12 ((::data/fn updated-leaf) {:x 5})) - "update-leaf should modify the function by composing with the given function")))) + ; update-leaf should preserve other leaf properties + (is (= expected-props (select-keys updated-leaf (keys expected-props)))) + ; update-leaf should modify the function by composing with the given function + (is (= 12 ((::data/fn updated-leaf) {:x 5})))))) (deftest update-branch-test (testing "update-branch without apply-to-condition updates truthy and falsey branches" @@ -137,4 +137,4 @@ updated-node (data/update-node leaf-node (partial * 2))] (is (= (::data/type updated-node) :leaf)) (is (= (::data/inputs updated-node) #{:x})) - (is (= 12 ((::data/fn updated-node) {:x 5})))))) + (is (= 12 ((::data/fn updated-node) {:x 5})))))) \ No newline at end of file From 83fc8dc52129bbeb1e972fe1bbbf69d463633ab5 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Thu, 3 Jul 2025 15:36:01 -0300 Subject: [PATCH 05/23] rm comment code --- src/nodely/engine/applicative.clj | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/nodely/engine/applicative.clj b/src/nodely/engine/applicative.clj index e7acaa1..0c18820 100644 --- a/src/nodely/engine/applicative.clj +++ b/src/nodely/engine/applicative.clj @@ -97,11 +97,4 @@ (merge env (reduce (fn [acc [k v]] (assoc acc k (protocols/-extract v))) {} - (lazy-env/scheduled-nodes lazy-env))))) - -(comment - - {:a (>leaf (throw (Exception. "My exception"))) - :b (>leaf 2) - :c (>leaf (+ ?a ?b))} - ) + (lazy-env/scheduled-nodes lazy-env))))) \ No newline at end of file From 6b091bbf6a3fbaa82131778660bd54f9af04d573 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Thu, 3 Jul 2025 15:36:07 -0300 Subject: [PATCH 06/23] fix lint --- test/nodely/api_test.clj | 12 ++++++------ test/nodely/data_test.clj | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/nodely/api_test.clj b/test/nodely/api_test.clj index 6bfb810..1727e86 100644 --- a/test/nodely/api_test.clj +++ b/test/nodely/api_test.clj @@ -6,7 +6,7 @@ [clojure.test :refer :all] [criterium.core :as criterium] [matcher-combinators.matchers :as matchers] - [nodely.api.v0 :as api :refer [>leaf >sequence >value blocking >if]] + [nodely.api.v0 :as api :refer [>if >leaf >sequence >value blocking]] [nodely.test-helpers :as t])) (def env {:x (>value 2) @@ -257,7 +257,7 @@ :z (>leaf (+ ?x ?y))} updated-env (update original-env :z api/update-node (partial * 2))] (t/matching 10 (api/eval-key updated-env :z {::api/engine engine-key})))) - + (t/testing "update-node and then eval a key that is not affected by the update-node" (let [original-env {:x (>value 2) :y (>value 3) @@ -265,26 +265,26 @@ updated-env (update original-env :z api/update-node (partial * 2))] (t/matching 2 (api/eval-key updated-env :x {::api/engine engine-key})) (t/matching 3 (api/eval-key updated-env :y {::api/engine engine-key})))) - + (t/testing "update-node with value node" (let [original-env {:x (>value 5) :y (>leaf (* ?x 2))} updated-env (update original-env :x api/update-node (partial + 3))] (t/matching 8 (api/eval-key updated-env :x {::api/engine engine-key})) (t/matching 16 (api/eval-key updated-env :y {::api/engine engine-key})))) - + (t/testing "update-node with sequence node" (let [original-env {:x (>value [1 2 3]) :y (>sequence inc ?x)} updated-env (update original-env :y api/update-node (fn [f] (comp (partial * 2) f)))] (t/matching [4 6 8] (api/eval-key updated-env :y {::api/engine engine-key})))) - + (t/testing "update-node with branch node" (let [original-env {:x (>value 5) :y (>if (>leaf (even? ?x)) (>value "even") (>value "odd"))} updated-env (update original-env :y api/update-node (partial str "result: "))] (t/matching "result: odd" (api/eval-key updated-env :y {::api/engine engine-key})))) - + (t/testing "update-node with apply-to-condition option" (let [original-env {:x (>value 0) :y (>if (>leaf ?x) (>value -10) (>value 20))} diff --git a/test/nodely/data_test.clj b/test/nodely/data_test.clj index 9701149..99cc2e7 100644 --- a/test/nodely/data_test.clj +++ b/test/nodely/data_test.clj @@ -54,7 +54,7 @@ truthy (data/leaf [:y] (comp inc :y)) falsey (data/leaf [:z] (comp inc :z)) branch (data/branch condition truthy falsey) - updated-branch (data/update-branch branch (partial * 2) {:apply-to-condition? true})] + updated-branch (data/update-branch branch (partial * 2) {:apply-to-condition? true})] (is (= 12 ((::data/fn (::data/condition updated-branch)) {:x 5}))) ; (* 2 (inc 5)) = 12 (is (= 12 ((::data/fn (::data/truthy updated-branch)) {:y 5}))) ; (* 2 (inc 5)) = 12 (is (= 12 ((::data/fn (::data/falsey updated-branch)) {:z 5}))))) ; (* 2 (inc 5)) = 12 From 84a1c01110591d4781100a45abc7476d1832c722 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Thu, 31 Jul 2025 15:33:39 -0300 Subject: [PATCH 07/23] add first version of try-env macro --- src/nodely/api/v0.clj | 7 +++++++ src/nodely/data.clj | 2 +- test/nodely/api_test.clj | 10 +++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/nodely/api/v0.clj b/src/nodely/api/v0.clj index 15e1d02..d01e2ed 100644 --- a/src/nodely/api/v0.clj +++ b/src/nodely/api/v0.clj @@ -27,6 +27,13 @@ (import-fn nodely.data/get-value get-value) (import-fn nodely.data/update-node update-node) +(defmacro try-env + [env & body] + `(update-vals ~env #(update-node % (fn[f#] (fn [& args#] (try (apply f# args#) ~@body))) {:apply-to-condition? true})) + ;; `(try ~env + ;; ~@body) + ) + (def virtual-future-failure (delay (try (import java.util.concurrent.ThreadPerTaskExecutor) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index 0df46d1..f2b3b3a 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -118,7 +118,7 @@ (defn update-leaf [leaf f] - (update leaf :nodely.data/fn #(comp f %))) + (update leaf :nodely.data/fn #(f %))) ;; TODO: We should change this --> `comp` was not working and we need to figure out another way to do this (declare update-node) diff --git a/test/nodely/api_test.clj b/test/nodely/api_test.clj index 1727e86..7ca79bc 100644 --- a/test/nodely/api_test.clj +++ b/test/nodely/api_test.clj @@ -301,4 +301,12 @@ :applicative.virtual-future))] (for [engine (set/difference (set (keys api/engine-data)) remove-keys)] - (update-node-engine-test-suite engine)))) \ No newline at end of file + (update-node-engine-test-suite engine)))) + +(t/deftest try-env + (t/testing "try-env works" + (t/matching 3 + (api/eval-key + (api/try-env exceptions-all-the-way-down + (catch Throwable _ 0)) + :d {::api/engine :sync.lazy})))) \ No newline at end of file From 1a557d9302088357ce103c4b5848f0825fce7bbc Mon Sep 17 00:00:00 2001 From: Alex Redington Date: Thu, 21 Aug 2025 14:20:52 -0400 Subject: [PATCH 08/23] wip: completable future antics --- src/nodely/data.clj | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index f2b3b3a..46d18fe 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -2,7 +2,10 @@ (:refer-clojure :exclude [map sequence flatten]) (:require [clojure.set :as set] - [schema.core :as s])) + [schema.core :as s]) + (:import + [java.util.concurrent CompletableFuture CompletionException] + [java.util.function Function])) ;; ;; Node Definitions @@ -116,9 +119,19 @@ :sequence (recur (::process-node node) (conj inputs (::input node)))))) +(defn- jfunctionify + [^clojure.lang.IFn f] + (reify Function (apply [o] (.invoke f o)))) + (defn update-leaf [leaf f] - (update leaf :nodely.data/fn #(f %))) ;; TODO: We should change this --> `comp` was not working and we need to figure out another way to do this + ;; f new function, arg is old function + (update leaf :nodely.data/fn (fn [oldf] + (fn [arg] + (-> (CompleteableFuture/completedFuture arg) + (.thenApply (jfunctionify oldf)) + (.thenApply (jfunctionify f)) + deref))))) (declare update-node) @@ -156,6 +169,11 @@ [env handler] (update-vals env #(update-node % handler {:apply-to-condition? true}))) +(defmacro try-env + [env & body] + `(try ~env + ~@body)) + (s/defn get-value :- s/Any [env :- Env k :- s/Keyword] From fc1c5a944ce8a70a88daacc8eb0c095a1db95abd Mon Sep 17 00:00:00 2001 From: Alex Redington Date: Thu, 21 Aug 2025 14:56:21 -0400 Subject: [PATCH 09/23] passings tests with cf based function composition allows (but doesn't test) exception catching composition --- src/nodely/data.clj | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index 46d18fe..ea5ff8e 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -4,6 +4,7 @@ [clojure.set :as set] [schema.core :as s]) (:import + [clojure.lang IFn] [java.util.concurrent CompletableFuture CompletionException] [java.util.function Function])) @@ -119,19 +120,44 @@ :sequence (recur (::process-node node) (conj inputs (::input node)))))) +;; Completablefuture based exception and return composition, need to enable try-env next +;; try-env needs +;; unwrap exceptions (always CompletionException thrown from CF wrapping actual exception) +;; determine if Exception type was specified to be caught (including inheritence, order of expression matters in try catch, can't use a map!) +;; Run body with caught exception and yield result + (defn- jfunctionify [^clojure.lang.IFn f] - (reify Function (apply [o] (.invoke f o)))) + (reify Function (apply [_ o] (.invoke f o)))) + +(defprotocol ComposableInvokable + (compose-return [ci f] "Returns a new ComposableInvokable that will invoke f on the return of cf, when it returns successfully.") + (compose-throw [ci f] "Returns a new ComposableInvokable that will invoke f on the exception thrown from ci, when it ends exceptionally.")) + +(deftype CompletableFutureComposableInvokable [compositions] + ComposableInvokable + (compose-return [_ f] (CompletableFutureComposableInvokable. (conj compositions [:apply f]))) + (compose-throw [_ f] (CompletableFutureComposableInvokable. (conj compositions [:throw f]))) + IFn + (invoke [_ arg] + (loop [cf (CompletableFuture/completedFuture arg) + [[op func] & rem] compositions] + (if (nil? func) + @cf + (recur (case op + :apply (.thenApply cf (jfunctionify func)) + :throw (.exceptionally cf (jfunctionify func))) + rem))))) + +(extend-protocol ComposableInvokable + IFn + (compose-return [fn f] (CompletableFutureComposableInvokable. (list [:apply fn] [:apply f]))) + (compose-throw [fn f] (CompletableFutureComposableInvokable. (list [:apply fn] [:throw f])))) (defn update-leaf [leaf f] ;; f new function, arg is old function - (update leaf :nodely.data/fn (fn [oldf] - (fn [arg] - (-> (CompleteableFuture/completedFuture arg) - (.thenApply (jfunctionify oldf)) - (.thenApply (jfunctionify f)) - deref))))) + (update leaf :nodely.data/fn compose-return f)) (declare update-node) From 0fedad2521381db29a4b87f590aa71503d054aff Mon Sep 17 00:00:00 2001 From: Alex Redington Date: Thu, 21 Aug 2025 15:08:48 -0400 Subject: [PATCH 10/23] Append to tail not head --- src/nodely/data.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index ea5ff8e..dc36dc1 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -151,8 +151,8 @@ (extend-protocol ComposableInvokable IFn - (compose-return [fn f] (CompletableFutureComposableInvokable. (list [:apply fn] [:apply f]))) - (compose-throw [fn f] (CompletableFutureComposableInvokable. (list [:apply fn] [:throw f])))) + (compose-return [fn f] (CompletableFutureComposableInvokable. [[:apply fn] [:apply f]])) + (compose-throw [fn f] (CompletableFutureComposableInvokable. [[:apply fn] [:throw f]]))) (defn update-leaf [leaf f] From 8400792c4f8fd595e7a47e8fda065b8a26670835 Mon Sep 17 00:00:00 2001 From: Alex Redington Date: Thu, 4 Sep 2025 14:32:03 -0400 Subject: [PATCH 11/23] Get rid of type shenanigans, just add catching and indirection --- src/nodely/data.clj | 81 +++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 47 deletions(-) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index dc36dc1..420f9a4 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -120,73 +120,60 @@ :sequence (recur (::process-node node) (conj inputs (::input node)))))) -;; Completablefuture based exception and return composition, need to enable try-env next -;; try-env needs -;; unwrap exceptions (always CompletionException thrown from CF wrapping actual exception) -;; determine if Exception type was specified to be caught (including inheritence, order of expression matters in try catch, can't use a map!) -;; Run body with caught exception and yield result - -(defn- jfunctionify - [^clojure.lang.IFn f] - (reify Function (apply [_ o] (.invoke f o)))) - -(defprotocol ComposableInvokable - (compose-return [ci f] "Returns a new ComposableInvokable that will invoke f on the return of cf, when it returns successfully.") - (compose-throw [ci f] "Returns a new ComposableInvokable that will invoke f on the exception thrown from ci, when it ends exceptionally.")) - -(deftype CompletableFutureComposableInvokable [compositions] - ComposableInvokable - (compose-return [_ f] (CompletableFutureComposableInvokable. (conj compositions [:apply f]))) - (compose-throw [_ f] (CompletableFutureComposableInvokable. (conj compositions [:throw f]))) - IFn - (invoke [_ arg] - (loop [cf (CompletableFuture/completedFuture arg) - [[op func] & rem] compositions] - (if (nil? func) - @cf - (recur (case op - :apply (.thenApply cf (jfunctionify func)) - :throw (.exceptionally cf (jfunctionify func))) - rem))))) - -(extend-protocol ComposableInvokable - IFn - (compose-return [fn f] (CompletableFutureComposableInvokable. [[:apply fn] [:apply f]])) - (compose-throw [fn f] (CompletableFutureComposableInvokable. [[:apply fn] [:throw f]]))) +(defn catching + "Like `clojure.core/comp`, except only affects throw completions of + the wrapped `g`. `f` must be a function of a single Exception, and + will be passed the exception thrown from `g` if `g` completes + exceptionally." + [f g] + (fn [& args] + (try (apply g args) + (catch Throwable t (f t))))) (defn update-leaf - [leaf f] + [leaf f compositor] ;; f new function, arg is old function - (update leaf :nodely.data/fn compose-return f)) + (update leaf :nodely.data/fn #(compositor f %))) -(declare update-node) +(declare env-update-helper) (defn update-branch [{::keys [condition truthy falsey]} f {:keys [apply-to-condition?] - :or {apply-to-condition? false}}] + :or {apply-to-condition? false} :as opts} + compositor] #::{:type :branch :condition (if apply-to-condition? - (update-node condition f) + (env-update-helper condition f opts compositor) condition) - :falsey (update-node falsey f) - :truthy (update-node truthy f)}) + :falsey (env-update-helper falsey f opts compositor) + :truthy (env-update-helper truthy f opts compositor)}) (defn update-sequence - [sequence f] - (update sequence ::process-node update-node f)) + [sequence f compositor] + (update sequence ::process-node env-update-helper f {} compositor)) + +(defn env-update-helper + [node f opts compositor] + (case (::type node) + :value (update node ::value f) + :leaf (update-leaf node f compositor) + :branch (update-branch node f opts compositor) + :sequence (update-sequence node f compositor))) (defn update-node ([node f opts] - (case (::type node) - :value (update node ::value f) - :leaf (update-leaf node f) - :branch (update-branch node f opts) - :sequence (update-sequence node f))) + (env-update-helper node f opts comp)) ([node f] (update-node node f {}))) +(defn catch-node + ([node f opts] + (env-update-helper node f opts catching)) + ([node f] + (catch-node node f {}))) + ;; ;; Env Utils ;; From 98d9d9909fad31e266594f09b7fefe35e19457e1 Mon Sep 17 00:00:00 2001 From: Sophia Velten de Melo Date: Thu, 11 Sep 2025 15:32:53 -0300 Subject: [PATCH 12/23] write test for with-error-handler, try to work on try-macro (wip) --- src/nodely/data.clj | 22 +++++++++++-- test/nodely/data_test.clj | 69 +++++---------------------------------- 2 files changed, 28 insertions(+), 63 deletions(-) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index 420f9a4..72b157d 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -180,9 +180,25 @@ (defn with-error-handler [env handler] - (update-vals env #(update-node % handler {:apply-to-condition? true}))) - -(defmacro try-env + (update-vals env #(catch-node % handler {:apply-to-condition? true}))) + +(defn with-try-clause-expr + [[_ t s expr]] + (fn [error] + (when (instance? error type) + expr))) + +;; '(with-try env (catch type symbol expr)) +;; (update-vals env (fn [error] (if (instance? error type) +;; () +;; )) +(defn with-try-expr + [env & clauses] + (let [clauses (for [[catch t s expr] clauses] + (do (assert (= catch 'catch)) + [t s expr]))])) + +(defmacro with-try [env & body] `(try ~env ~@body)) diff --git a/test/nodely/data_test.clj b/test/nodely/data_test.clj index 99cc2e7..efcd17b 100644 --- a/test/nodely/data_test.clj +++ b/test/nodely/data_test.clj @@ -27,65 +27,6 @@ (data/leaf [:b] identity) (data/leaf [:c] identity))))))) -(deftest update-leaf-test - (testing "update-leaf modifies the function by composing with the given function" - (let [original-leaf (data/leaf [:x] (comp inc :x)) - updated-leaf (data/update-leaf original-leaf (partial * 2)) - expected-props {::data/type :leaf - ::data/inputs #{:x}}] - ; update-leaf should preserve other leaf properties - (is (= expected-props (select-keys updated-leaf (keys expected-props)))) - ; update-leaf should modify the function by composing with the given function - (is (= 12 ((::data/fn updated-leaf) {:x 5})))))) - -(deftest update-branch-test - (testing "update-branch without apply-to-condition updates truthy and falsey branches" - (let [condition (data/leaf [:x] (comp even? :x)) - truthy (data/leaf [:y] (comp inc :y)) - falsey (data/leaf [:z] (comp dec :z)) - branch (data/branch condition truthy falsey) - updated-branch (data/update-branch branch (partial * 2) {})] - (is (= true ((::data/fn (::data/condition updated-branch)) {:x 2}))) ; (even? 2) = true - (is (= 20 ((::data/fn (::data/truthy updated-branch)) {:y 9}))) ; (* 2 (inc 9)) = 20 - (is (= 18 ((::data/fn (::data/falsey updated-branch)) {:z 10}))))) ; (* 2 (dec 10)) = 18 - - (testing "update-branch with apply-to-condition updates all branches including condition" - (let [condition (data/leaf [:x] (comp inc :x)) - truthy (data/leaf [:y] (comp inc :y)) - falsey (data/leaf [:z] (comp inc :z)) - branch (data/branch condition truthy falsey) - updated-branch (data/update-branch branch (partial * 2) {:apply-to-condition? true})] - (is (= 12 ((::data/fn (::data/condition updated-branch)) {:x 5}))) ; (* 2 (inc 5)) = 12 - (is (= 12 ((::data/fn (::data/truthy updated-branch)) {:y 5}))) ; (* 2 (inc 5)) = 12 - (is (= 12 ((::data/fn (::data/falsey updated-branch)) {:z 5}))))) ; (* 2 (inc 5)) = 12 - - (testing "update-branch with nested branches updates recursively" - (let [inner-condition (data/leaf [:a] (comp even? :a)) - inner-truthy (data/leaf [:b] (comp inc :b)) - inner-falsey (data/leaf [:c] (comp dec :c)) - inner-branch (data/branch inner-condition inner-truthy inner-falsey) - outer-condition (data/leaf [:x] (comp even? :x)) - outer-truthy inner-branch - outer-falsey (data/leaf [:z] (comp dec :z)) - outer-branch (data/branch outer-condition outer-truthy outer-falsey) - updated-branch (data/update-branch outer-branch (partial * 2) {})] - ;; Outer condition should not be updated by default - (is (= true ((::data/fn (::data/condition updated-branch)) {:x 2}))) ; (even? 2) = true - ;; Inner condition should not be updated by default - (is (= true ((::data/fn (::data/condition (::data/truthy updated-branch))) {:a 2}))) ; (even? 2) = true - ;; Inner truthy should be updated - (is (= 12 ((::data/fn (::data/truthy (::data/truthy updated-branch))) {:b 5}))) ; (* 2 (inc 5)) = 12 - ;; Inner falsey should be updated - (is (= 8 ((::data/fn (::data/falsey (::data/truthy updated-branch))) {:c 5}))) ; (* 2 (dec 5)) = 8 - ;; Outer falsey should be updated - (is (= 8 ((::data/fn (::data/falsey updated-branch)) {:z 5})))))) ; (* 2 (dec 5)) = 8 - -(deftest update-sequence-test - (testing "update-sequence composes the function argument" - (let [sequence-node (data/sequence :items [:closure-var] (comp inc :closure-var) #{}) - updated-sequence (data/update-sequence sequence-node (partial * 2))] - (is (= 12 ((::data/fn (::data/process-node updated-sequence)) {:closure-var 5})))))) ; (* 2 (inc 5)) = 12 - (deftest update-node-test (testing "update-node with value node" (let [value-node (data/value 42) @@ -137,4 +78,12 @@ updated-node (data/update-node leaf-node (partial * 2))] (is (= (::data/type updated-node) :leaf)) (is (= (::data/inputs updated-node) #{:x})) - (is (= 12 ((::data/fn updated-node) {:x 5})))))) \ No newline at end of file + (is (= 12 ((::data/fn updated-node) {:x 5})))))) + +(deftest with-error-handler-test + (testing "with-error-handler wraps leaf functions to handle thrown exceptions" + (let [throw-env {:a (data/leaf #{} (fn [] (throw (ex-info "OOps" {}))))} + handled-env (data/with-error-handler throw-env (fn [^Throwable _] :handled))] + (is (= :handleds ((::data/fn (get handled-env :a)))))))) + +{:a (data/leaf #{} (fn [] (throw (ex-info "OOps" {}))))} From 620dae04397416566107c4d89e2922acb2a9c97b Mon Sep 17 00:00:00 2001 From: Sophia Velten de Melo Date: Thu, 11 Sep 2025 15:43:50 -0300 Subject: [PATCH 13/23] fix test --- test/nodely/data_test.clj | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/nodely/data_test.clj b/test/nodely/data_test.clj index efcd17b..90064c8 100644 --- a/test/nodely/data_test.clj +++ b/test/nodely/data_test.clj @@ -1,8 +1,7 @@ (ns nodely.data-test (:refer-clojure :exclude [cond]) - (:require - [clojure.test :refer :all] - [nodely.data :as data])) + (:require [clojure.test :refer :all] + [nodely.data :as data])) (deftest node? (testing "an actual node" @@ -84,6 +83,6 @@ (testing "with-error-handler wraps leaf functions to handle thrown exceptions" (let [throw-env {:a (data/leaf #{} (fn [] (throw (ex-info "OOps" {}))))} handled-env (data/with-error-handler throw-env (fn [^Throwable _] :handled))] - (is (= :handleds ((::data/fn (get handled-env :a)))))))) + (is (= :handled ((::data/fn (get handled-env :a)))))))) {:a (data/leaf #{} (fn [] (throw (ex-info "OOps" {}))))} From b29857c5cf0471cbf89a1d1e95b9047cd3dbbe5b Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Thu, 18 Sep 2025 15:02:28 -0300 Subject: [PATCH 14/23] initial implementation of with-try macro --- src/nodely/data.clj | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index 72b157d..33f4f0a 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -182,26 +182,42 @@ [env handler] (update-vals env #(catch-node % handler {:apply-to-condition? true}))) -(defn with-try-clause-expr - [[_ t s expr]] - (fn [error] - (when (instance? error type) - expr))) +#_(defn with-try-clause-expr + [[_ t s expr]] + (fn [error] + (when (instance? error type) + expr))) ;; '(with-try env (catch type symbol expr)) ;; (update-vals env (fn [error] (if (instance? error type) ;; () ;; )) +#_(defn with-try-expr + [env & clauses] + (let [clauses (for [[catch t s expr] clauses] + (do (assert (= catch 'catch)) + [t s expr]))])) + (defn with-try-expr - [env & clauses] - (let [clauses (for [[catch t s expr] clauses] - (do (assert (= catch 'catch)) - [t s expr]))])) + [clauses] + (let [clauses (into {} (for [[c t s expr] clauses] + (do (assert (= c 'catch)) + [(resolve t) (eval `(fn [~s] ~expr))] + #_[t s expr])))] + clauses)) (defmacro with-try [env & body] - `(try ~env - ~@body)) + `(with-error-handler + ~env + ~(with-try-expr body))) + +(comment + (macroexpand-1 '(with-try {:a "hi"} + (catch Exception e (println e)) + (catch Throwable t (println t)))) + ; + ) (s/defn get-value :- s/Any [env :- Env From 3e37a8a703e7f64d5af921200e9d3d17cbf853de Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Mon, 13 Oct 2025 13:54:11 -0300 Subject: [PATCH 15/23] add working version of with-try macro with some initial tests --- src/nodely/api/v0.clj | 1 + src/nodely/data.clj | 22 +++++++++++++++++----- test/nodely/api_test.clj | 31 ++++++++++++++++++++++++------- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/nodely/api/v0.clj b/src/nodely/api/v0.clj index d01e2ed..9c5d3da 100644 --- a/src/nodely/api/v0.clj +++ b/src/nodely/api/v0.clj @@ -20,6 +20,7 @@ nodely.data/leaf nodely.data/sequence nodely.data/branch + nodely.data/with-try engine-core/checked-env) (import-fn nodely.engine.lazy/eval-node-with-values eval-node-with-values) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index 33f4f0a..a574c64 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -198,24 +198,36 @@ (do (assert (= catch 'catch)) [t s expr]))])) +(defn tuple-to-handler + [m] + (fn [error] + (if-let [f (some (fn [[ex-class handler]] (when (instance? ex-class error) handler)) m)] + (f error) + (throw error)))) + (defn with-try-expr [clauses] - (let [clauses (into {} (for [[c t s expr] clauses] + (let [clauses (into [] (for [[c t s expr] clauses] (do (assert (= c 'catch)) - [(resolve t) (eval `(fn [~s] ~expr))] - #_[t s expr])))] + (if-let [t (resolve t)] + [t (eval `(fn [~s] ~expr))] + (throw (ex-info (str "Could not resolve exception class: " t) {:type t}))))))] clauses)) (defmacro with-try [env & body] `(with-error-handler ~env - ~(with-try-expr body))) + (tuple-to-handler ~(with-try-expr body)))) (comment - (macroexpand-1 '(with-try {:a "hi"} + (macroexpand-1 '(with-try {:a (leaf [:x] (comp inc :x))} (catch Exception e (println e)) (catch Throwable t (println t)))) + + (tuple-to-handler {java.lang.Exception identity, + java.lang.Throwable identity}) + ; ) diff --git a/test/nodely/api_test.clj b/test/nodely/api_test.clj index 7ca79bc..1ae7be9 100644 --- a/test/nodely/api_test.clj +++ b/test/nodely/api_test.clj @@ -303,10 +303,27 @@ remove-keys)] (update-node-engine-test-suite engine)))) -(t/deftest try-env - (t/testing "try-env works" - (t/matching 3 - (api/eval-key - (api/try-env exceptions-all-the-way-down - (catch Throwable _ 0)) - :d {::api/engine :sync.lazy})))) \ No newline at end of file +(t/deftest with-try + (t/testing "with-try works Throwable catches first" + (t/matching "Could not resolve exception class: NonSenseException" + (try + (eval '(api/with-try exceptions-all-the-way-down + (catch NonSenseException _ 0))) + (catch clojure.lang.Compiler$CompilerException e + (ex-message (.getCause e)))))) + + (t/testing "with-try works Throwable catches first" + (t/matching 0 + (api/eval-key + (api/with-try exceptions-all-the-way-down + (catch Throwable _ -3) + (catch clojure.lang.ExceptionInfo _ 0)) + :d {::api/engine :sync.lazy}))) + + (t/testing "with-try works ExceptionInfo catches first" + (t/matching 0 + (api/eval-key + (api/with-try exceptions-all-the-way-down + (catch clojure.lang.ExceptionInfo _ -3) + (catch Throwable _ 0)) + :d {::api/engine :sync.lazy})))) From e770e0de358cb2ee4077b428548ea4eee813da9b Mon Sep 17 00:00:00 2001 From: Alex Redington Date: Thu, 16 Oct 2025 13:42:53 -0400 Subject: [PATCH 16/23] Quote prevents aliases from resolving --- test/nodely/api_test.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nodely/api_test.clj b/test/nodely/api_test.clj index 1ae7be9..a1ca551 100644 --- a/test/nodely/api_test.clj +++ b/test/nodely/api_test.clj @@ -307,7 +307,7 @@ (t/testing "with-try works Throwable catches first" (t/matching "Could not resolve exception class: NonSenseException" (try - (eval '(api/with-try exceptions-all-the-way-down + (eval '(nodely.api.v0/with-try exceptions-all-the-way-down (catch NonSenseException _ 0))) (catch clojure.lang.Compiler$CompilerException e (ex-message (.getCause e)))))) From 0d75eb3dd8579a3c81d9b95d2e27b4eac391e548 Mon Sep 17 00:00:00 2001 From: Alex Redington Date: Thu, 16 Oct 2025 13:44:20 -0400 Subject: [PATCH 17/23] lint fix --- src/nodely/api/v0.clj | 2 +- src/nodely/data.clj | 6 +----- test/nodely/data_test.clj | 5 +++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/nodely/api/v0.clj b/src/nodely/api/v0.clj index 9c5d3da..b29606f 100644 --- a/src/nodely/api/v0.clj +++ b/src/nodely/api/v0.clj @@ -30,7 +30,7 @@ (defmacro try-env [env & body] - `(update-vals ~env #(update-node % (fn[f#] (fn [& args#] (try (apply f# args#) ~@body))) {:apply-to-condition? true})) + `(update-vals ~env #(update-node % (fn [f#] (fn [& args#] (try (apply f# args#) ~@body))) {:apply-to-condition? true})) ;; `(try ~env ;; ~@body) ) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index a574c64..fd0e544 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -2,11 +2,7 @@ (:refer-clojure :exclude [map sequence flatten]) (:require [clojure.set :as set] - [schema.core :as s]) - (:import - [clojure.lang IFn] - [java.util.concurrent CompletableFuture CompletionException] - [java.util.function Function])) + [schema.core :as s])) ;; ;; Node Definitions diff --git a/test/nodely/data_test.clj b/test/nodely/data_test.clj index 90064c8..291f7f2 100644 --- a/test/nodely/data_test.clj +++ b/test/nodely/data_test.clj @@ -1,7 +1,8 @@ (ns nodely.data-test (:refer-clojure :exclude [cond]) - (:require [clojure.test :refer :all] - [nodely.data :as data])) + (:require + [clojure.test :refer :all] + [nodely.data :as data])) (deftest node? (testing "an actual node" From b5bd14d520286d930d9f8442da2f59cccaa9b9d7 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Thu, 16 Oct 2025 15:04:35 -0300 Subject: [PATCH 18/23] clean up code and make tests use engine-test-suite --- src/nodely/api/v0.clj | 8 ---- src/nodely/data.clj | 27 ----------- test/nodely/api_test.clj | 96 +++++++++++---------------------------- test/nodely/data_test.clj | 2 - 4 files changed, 26 insertions(+), 107 deletions(-) diff --git a/src/nodely/api/v0.clj b/src/nodely/api/v0.clj index b29606f..58b92dd 100644 --- a/src/nodely/api/v0.clj +++ b/src/nodely/api/v0.clj @@ -26,14 +26,6 @@ (import-fn nodely.engine.lazy/eval-node-with-values eval-node-with-values) (import-fn nodely.data/merge-values merge-values) (import-fn nodely.data/get-value get-value) -(import-fn nodely.data/update-node update-node) - -(defmacro try-env - [env & body] - `(update-vals ~env #(update-node % (fn [f#] (fn [& args#] (try (apply f# args#) ~@body))) {:apply-to-condition? true})) - ;; `(try ~env - ;; ~@body) - ) (def virtual-future-failure (delay diff --git a/src/nodely/data.clj b/src/nodely/data.clj index fd0e544..7ec6f09 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -178,22 +178,6 @@ [env handler] (update-vals env #(catch-node % handler {:apply-to-condition? true}))) -#_(defn with-try-clause-expr - [[_ t s expr]] - (fn [error] - (when (instance? error type) - expr))) - -;; '(with-try env (catch type symbol expr)) -;; (update-vals env (fn [error] (if (instance? error type) -;; () -;; )) -#_(defn with-try-expr - [env & clauses] - (let [clauses (for [[catch t s expr] clauses] - (do (assert (= catch 'catch)) - [t s expr]))])) - (defn tuple-to-handler [m] (fn [error] @@ -216,17 +200,6 @@ ~env (tuple-to-handler ~(with-try-expr body)))) -(comment - (macroexpand-1 '(with-try {:a (leaf [:x] (comp inc :x))} - (catch Exception e (println e)) - (catch Throwable t (println t)))) - - (tuple-to-handler {java.lang.Exception identity, - java.lang.Throwable identity}) - - ; - ) - (s/defn get-value :- s/Any [env :- Env k :- s/Keyword] diff --git a/test/nodely/api_test.clj b/test/nodely/api_test.clj index a1ca551..241d89c 100644 --- a/test/nodely/api_test.clj +++ b/test/nodely/api_test.clj @@ -247,53 +247,34 @@ (try (api/eval-node-channel env (>leaf (inc ?z)) {::api/engine :core.async.doesnt-exist}) (catch clojure.lang.ExceptionInfo e (ex-data e))))))) -(defn update-node-engine-test-suite +(defn with-try-engine-test-suite [engine-key] (t/testing (name engine-key) - (t/testing "update-node functionality" - (t/testing "update-node and then eval a key that is affected by the update-node" - (let [original-env {:x (>value 2) - :y (>value 3) - :z (>leaf (+ ?x ?y))} - updated-env (update original-env :z api/update-node (partial * 2))] - (t/matching 10 (api/eval-key updated-env :z {::api/engine engine-key})))) + (t/testing "Exception types must be resolvable at with-try expansion time" + (t/matching "Could not resolve exception class: NonSenseException" + (try + (eval '(nodely.api.v0/with-try exceptions-all-the-way-down + (catch NonSenseException _ 0))) + (catch clojure.lang.Compiler$CompilerException e + (ex-message (.getCause e)))))) + + (t/testing "with-try catches exceptions informed by class inheritance and catch clause ordering" + (t/matching 0 + (api/eval-key + (api/with-try exceptions-all-the-way-down + (catch Throwable _ -3) + (catch clojure.lang.ExceptionInfo _ 0)) + :d {::api/engine engine-key}))) + + (t/testing "with-try order of clauses can preempt inheritance" + (t/matching 0 + (api/eval-key + (api/with-try exceptions-all-the-way-down + (catch clojure.lang.ExceptionInfo _ -3) + (catch Throwable _ 0)) + :d {::api/engine engine-key}))))) - (t/testing "update-node and then eval a key that is not affected by the update-node" - (let [original-env {:x (>value 2) - :y (>value 3) - :z (>leaf (+ ?x ?y))} - updated-env (update original-env :z api/update-node (partial * 2))] - (t/matching 2 (api/eval-key updated-env :x {::api/engine engine-key})) - (t/matching 3 (api/eval-key updated-env :y {::api/engine engine-key})))) - - (t/testing "update-node with value node" - (let [original-env {:x (>value 5) - :y (>leaf (* ?x 2))} - updated-env (update original-env :x api/update-node (partial + 3))] - (t/matching 8 (api/eval-key updated-env :x {::api/engine engine-key})) - (t/matching 16 (api/eval-key updated-env :y {::api/engine engine-key})))) - - (t/testing "update-node with sequence node" - (let [original-env {:x (>value [1 2 3]) - :y (>sequence inc ?x)} - updated-env (update original-env :y api/update-node (fn [f] (comp (partial * 2) f)))] - (t/matching [4 6 8] (api/eval-key updated-env :y {::api/engine engine-key})))) - - (t/testing "update-node with branch node" - (let [original-env {:x (>value 5) - :y (>if (>leaf (even? ?x)) (>value "even") (>value "odd"))} - updated-env (update original-env :y api/update-node (partial str "result: "))] - (t/matching "result: odd" (api/eval-key updated-env :y {::api/engine engine-key})))) - - (t/testing "update-node with apply-to-condition option" - (let [original-env {:x (>value 0) - :y (>if (>leaf ?x) (>value -10) (>value 20))} - updated-env (update original-env :y api/update-node pos? {:apply-to-condition? true})] - ;; original: ?x = 0 is truthy, so truthy branch => -10 - ;; after update: (pos? ?x) => (pos? 0) => false, so takes falsey branch => (pos? 20) => true - (t/matching true (api/eval-key updated-env :y {::api/engine engine-key}))))))) - -(t/deftest update-node-test +(t/deftest with-try (let [remove-keys (conj #{:core-async.iterative-scheduling :async.virtual-futures} (when (try (import java.util.concurrent.ThreadPerTaskExecutor) @@ -301,29 +282,4 @@ :applicative.virtual-future))] (for [engine (set/difference (set (keys api/engine-data)) remove-keys)] - (update-node-engine-test-suite engine)))) - -(t/deftest with-try - (t/testing "with-try works Throwable catches first" - (t/matching "Could not resolve exception class: NonSenseException" - (try - (eval '(nodely.api.v0/with-try exceptions-all-the-way-down - (catch NonSenseException _ 0))) - (catch clojure.lang.Compiler$CompilerException e - (ex-message (.getCause e)))))) - - (t/testing "with-try works Throwable catches first" - (t/matching 0 - (api/eval-key - (api/with-try exceptions-all-the-way-down - (catch Throwable _ -3) - (catch clojure.lang.ExceptionInfo _ 0)) - :d {::api/engine :sync.lazy}))) - - (t/testing "with-try works ExceptionInfo catches first" - (t/matching 0 - (api/eval-key - (api/with-try exceptions-all-the-way-down - (catch clojure.lang.ExceptionInfo _ -3) - (catch Throwable _ 0)) - :d {::api/engine :sync.lazy})))) + (with-try-engine-test-suite engine)))) diff --git a/test/nodely/data_test.clj b/test/nodely/data_test.clj index 291f7f2..742dc74 100644 --- a/test/nodely/data_test.clj +++ b/test/nodely/data_test.clj @@ -85,5 +85,3 @@ (let [throw-env {:a (data/leaf #{} (fn [] (throw (ex-info "OOps" {}))))} handled-env (data/with-error-handler throw-env (fn [^Throwable _] :handled))] (is (= :handled ((::data/fn (get handled-env :a)))))))) - -{:a (data/leaf #{} (fn [] (throw (ex-info "OOps" {}))))} From 140e42d95684907022e89d0b78719c5876596f92 Mon Sep 17 00:00:00 2001 From: joaopluigi Date: Thu, 16 Oct 2025 15:06:03 -0300 Subject: [PATCH 19/23] fix lint --- test/nodely/api_test.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nodely/api_test.clj b/test/nodely/api_test.clj index 241d89c..7cccda0 100644 --- a/test/nodely/api_test.clj +++ b/test/nodely/api_test.clj @@ -6,7 +6,7 @@ [clojure.test :refer :all] [criterium.core :as criterium] [matcher-combinators.matchers :as matchers] - [nodely.api.v0 :as api :refer [>if >leaf >sequence >value blocking]] + [nodely.api.v0 :as api :refer [>leaf >sequence >value blocking]] [nodely.test-helpers :as t])) (def env {:x (>value 2) From 30d8064bfef505fa65e62fe9f01135375e8c2a5d Mon Sep 17 00:00:00 2001 From: sovelten <7022879+sovelten@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:11:16 -0300 Subject: [PATCH 20/23] Update nodely to 2.0.3 --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 76def45..c6a5df6 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject dev.nu/nodely "2.0.2" +(defproject dev.nu/nodely "2.0.3" :description "Decoupling data fetching from data dependency declaration" :url "https://github.com/nubank/nodely" :license {:name "MIT"} From 661ad896d2ea2bd1d0ecc165acfd4dcc2b11d397 Mon Sep 17 00:00:00 2001 From: sovelten <7022879+sovelten@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:13:12 -0300 Subject: [PATCH 21/23] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 381cd83..9a307e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 2.0.3 / 2024-11-14 +- Add with-try macro to nodely.api.v0, enabling adding an exception handler globally to an environment. + ## 2.0.2 / 2024-11-14 - Remove the default engine behavior in internal applicative eval functions From 61c616ef85f60736033f442fa19e2e9164bd6b93 Mon Sep 17 00:00:00 2001 From: Alex Redington Date: Thu, 30 Oct 2025 14:21:59 -0400 Subject: [PATCH 22/23] Make impl details private, add docstring to `with-try` --- src/nodely/data.clj | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/nodely/data.clj b/src/nodely/data.clj index 7ec6f09..057275a 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -178,14 +178,14 @@ [env handler] (update-vals env #(catch-node % handler {:apply-to-condition? true}))) -(defn tuple-to-handler +(defn- tuple-to-handler [m] (fn [error] (if-let [f (some (fn [[ex-class handler]] (when (instance? ex-class error) handler)) m)] (f error) (throw error)))) -(defn with-try-expr +(defn- with-try-expr [clauses] (let [clauses (into [] (for [[c t s expr] clauses] (do (assert (= c 'catch)) @@ -195,6 +195,31 @@ clauses)) (defmacro with-try + "Macro + + `with-try` will apply an error handling semantic to every leaf, + branch, and sequence node in a provided environment. If any such + nodes throw an exception, the catch expressions described in the + body of `with-try` will be evaluated. The resulting value of the + matching catch clause will become the value of the node which threw + the exception. + + `with-try` has syntax equivalent to Clojure's `try` special form for + `catch` clauses but does not currently support use of a `finally` + clause. + + `with-try` creates a policy across an entire environment + indiscriminately, when it is possible, clients are advised to catch + exceptional cases in the implementations of leaf/branch/sequence + nodes explicitly. + + example: + + (with-try {:a (>leaf (/ 5 ?b)) + :b (>value 0)} + (catch ArithmeticException _ 0)) + + will result in `:a` evaluating to 0" [env & body] `(with-error-handler ~env From 7dfedb4caa3674290e4badf1b33e9aed645f2818 Mon Sep 17 00:00:00 2001 From: Alex Redington Date: Thu, 30 Oct 2025 14:34:45 -0400 Subject: [PATCH 23/23] Document `with-try` in Readme --- README.md | 24 ++++++++++++++++++++++++ src/nodely/data.clj | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2001d3d..5be1c22 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,30 @@ Let's say that `x` and `y` are actually expensive http requests. What we want is This will evaluate the node by realizing all required dependencies, without executing the dependencies that are not needed. For instance, if x is even, y is not evaluated at all. +#### Environment Defined Exception Handling + +Nodely offers the `with-try` macro to provide an environment-level +policy for exception handling. This tool is essentially coarse grained +and will apply an exception handler to every leaf, branch, and +sequence node in the environment provided to it. The syntax mirrors +that of Clojure's `(try ... (catch ...))` special forms, e.g. + +```clojure + (with-try {:a (>leaf (/ 5 ?b)) + :b (>value 0)} + (catch ArithmeticException _ Double/NaN) + (catch NullPointerException _ 0)) +``` + +Multiple catch clauses may be specified to perform type based +dispatching of which expression an exceptional case should +trigger. + +This is offered as a tool for setting exceptional policy independently +of specifying environments; Nodely clients are advised to prefer +handling exceptional cases explicitly in the implementations of leaf, +branch and sequence functions when possible. + #### Testing Using `eval-node-with-values` is the most straightforward way to evaluate a node by supplying actual values for the data dependencies. If we want to test `branch-node` without actually making the expensive calls, it is as simple as that: diff --git a/src/nodely/data.clj b/src/nodely/data.clj index 057275a..b96d338 100644 --- a/src/nodely/data.clj +++ b/src/nodely/data.clj @@ -217,7 +217,8 @@ (with-try {:a (>leaf (/ 5 ?b)) :b (>value 0)} - (catch ArithmeticException _ 0)) + (catch ArithmeticException _ Double/NaN) + (catch NullPointerException _ 0)) will result in `:a` evaluating to 0" [env & body]