From f3305b36fa4c7b3e8293cae1328b4084d8651d0b Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Sat, 21 Feb 2026 14:14:54 +0200 Subject: [PATCH 1/9] Idea: possible optimization (some-> x :field) expands to (if (nil? x) nil (:field x)) so adds some unnecessary overhead when fields are never nil --- src/sci/impl/analyzer.cljc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sci/impl/analyzer.cljc b/src/sci/impl/analyzer.cljc index 68697fbd..d30dce42 100644 --- a/src/sci/impl/analyzer.cljc +++ b/src/sci/impl/analyzer.cljc @@ -1029,10 +1029,10 @@ args (object-array args) class-expr (:class-expr (meta expr))] ;; prefab static-methods - (if-let [f (some-> ctx :env deref - :class->opts :static-methods - (get (interop/fully-qualify-class ctx class-expr)) - (get method-expr))] + (if-let [f (-> ctx :env deref :class->opts + (some-> :static-methods + (get (interop/fully-qualify-class ctx class-expr)) + (get method-expr)))] (return-call ctx expr f (cons instance-expr args) stack nil) (sci.impl.types/->Node (interop/invoke-static-method ctx bindings instance-expr meth-name From 946ab87e761d9834433ca3f5a876999aad8de76e Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Sat, 21 Feb 2026 17:48:59 +0200 Subject: [PATCH 2/9] Add support for instance method override per class Does not interfere with :allow :all E.g. (sci/eval-string "(.toString \"your name\")" {:classes {'java.lang.String {:class java.lang.String :instance-methods {'toString (fn [_s] :dude)}}}}) --- src/sci/impl/evaluator.cljc | 40 ++++++++++++++++++++++--------------- test/sci/interop_test.cljc | 9 +++++++++ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/sci/impl/evaluator.cljc b/src/sci/impl/evaluator.cljc index bf290344..da3a3e90 100644 --- a/src/sci/impl/evaluator.cljc +++ b/src/sci/impl/evaluator.cljc @@ -145,22 +145,30 @@ class->opts (:class->opts env) allowed? (or #?(:cljs allowed) - (get class->opts :allow) - (let [instance-class-name #?(:clj (.getName ^Class instance-class) - :cljs (.-name instance-class)) - instance-class-symbol (symbol instance-class-name)] - (get class->opts instance-class-symbol))) - ^Class target-class (if allowed? instance-class - (when-let [f (:public-class env)] - (f instance-expr*)))] - ;; we have to check options at run time, since we don't know what the class - ;; of instance-expr is at analysis time - (when-not #?(:clj target-class - :cljs allowed?) - (throw-error-with-location (str "Method " method-str " on " instance-class " not allowed!") instance-expr)) - (if field-access - (interop/invoke-instance-field instance-expr* target-class method-str) - (interop/invoke-instance-method ctx bindings instance-expr* target-class method-str args arg-count arg-types)))))) + (get class->opts :allow)) + normal-interop + (fn [target-class] + (if field-access + (interop/invoke-instance-field instance-expr* target-class method-str) + (interop/invoke-instance-method ctx bindings instance-expr* target-class method-str args arg-count arg-types)))] + (if allowed? + (normal-interop instance-class) + (let [instance-class-name #?(:clj (.getName ^Class instance-class) + :cljs (.-name instance-class)) + instance-class-symbol (symbol instance-class-name)] + (if-let [class-config (get class->opts instance-class-symbol)] + (if-let [f (some-> class-config :instance-methods + (get (symbol method-str)))] + (apply f instance-expr* args) + (normal-interop instance-class)) + (let [^Class target-class (when-let [f (:public-class env)] + (f instance-expr*))] + + ;; we have to check options at run time, since we don't know what the class + ;; of instance-expr is at analysis time + (if target-class + (normal-interop target-class) + (throw-error-with-location (str "Method " method-str " on " instance-class " not allowed!") instance-expr)))))))))) ;;;; End interop diff --git a/test/sci/interop_test.cljc b/test/sci/interop_test.cljc index 05555344..fc955ac6 100644 --- a/test/sci/interop_test.cljc +++ b/test/sci/interop_test.cljc @@ -44,6 +44,15 @@ :public-class (fn [o] (when (instance? java.util.Map o) java.util.Map))}}))))))) + (testing "method override" + (is (= :dude (tu/eval* "(.toString \"your name\")" + {:classes {'java.lang.String + {:class java.lang.String + :instance-methods {'toString + ;; REVIEW should toString also receive the class like the :static-methods + (fn [_s] + :dude)}}}})))))) + #?(:clj (deftest instance-fields (is (= 3 (sci/eval-string "(.-x (PublicFields.))" {:classes {'PublicFields PublicFields}}))))) From 0643baf64a5386fb7ca7745ebe43f8bf84eda5e1 Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Sat, 21 Feb 2026 18:13:53 +0200 Subject: [PATCH 3/9] Add :deny option to deny any non-listed instance method --- src/sci/impl/evaluator.cljc | 10 +++++++--- test/sci/interop_test.cljc | 34 ++++++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/sci/impl/evaluator.cljc b/src/sci/impl/evaluator.cljc index da3a3e90..db974535 100644 --- a/src/sci/impl/evaluator.cljc +++ b/src/sci/impl/evaluator.cljc @@ -157,9 +157,13 @@ :cljs (.-name instance-class)) instance-class-symbol (symbol instance-class-name)] (if-let [class-config (get class->opts instance-class-symbol)] - (if-let [f (some-> class-config :instance-methods - (get (symbol method-str)))] - (apply f instance-expr* args) + (if-let [instance-methods (:instance-methods class-config)] + + (if-let [f (get instance-methods (symbol method-str))] + (apply f instance-expr* args) + (if (:deny instance-methods) + (throw-error-with-location (str "Method " method-str " on " instance-class " not allowed!") instance-expr) + (normal-interop instance-class))) (normal-interop instance-class)) (let [^Class target-class (when-let [f (:public-class env)] (f instance-expr*))] diff --git a/test/sci/interop_test.cljc b/test/sci/interop_test.cljc index fc955ac6..48bbf750 100644 --- a/test/sci/interop_test.cljc +++ b/test/sci/interop_test.cljc @@ -42,16 +42,30 @@ (is (= #{:a} (tu/eval* "(.keySet {:a 1})" {:classes {'java.util.Map 'java.util.Map :public-class (fn [o] - (when (instance? java.util.Map o) java.util.Map))}}))))))) - - (testing "method override" - (is (= :dude (tu/eval* "(.toString \"your name\")" - {:classes {'java.lang.String - {:class java.lang.String - :instance-methods {'toString - ;; REVIEW should toString also receive the class like the :static-methods - (fn [_s] - :dude)}}}})))))) + (when (instance? java.util.Map o) java.util.Map))}}))))) + + (testing "single method override" + (let [config {:classes {'java.lang.String + {:class java.lang.String + :instance-methods {'toString + ;; REVIEW should toString also receive the class like the functions in :static-methods + (fn [_s] + :dude)}}}}] + (is (= :dude (tu/eval* "(.toString \"your name\")" config))) + (is (= 9 (tu/eval* "(.length \"your name\")" config))))) + + + (testing "single method allowed" + (let [config {:classes {'java.lang.String + {:class java.lang.String + :instance-methods {:deny true + 'toString + ;; REVIEW should toString also receive the class like the functions in :static-methods + (fn [_s] + :dude)}}}}] + (is (= :dude (tu/eval* "(.toString \"your name\")" config))) + (is (thrown-with-msg? Exception #"allowed" + (tu/eval* "(.length \"your name\")" config))))))) #?(:clj (deftest instance-fields From 9ba68bd59130c4fc46f35cb00c9c7247174c1022 Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Sat, 21 Feb 2026 18:48:17 +0200 Subject: [PATCH 4/9] Don't call the new tests with Graal --- test/sci/interop_test.cljc | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/test/sci/interop_test.cljc b/test/sci/interop_test.cljc index 48bbf750..266f33b2 100644 --- a/test/sci/interop_test.cljc +++ b/test/sci/interop_test.cljc @@ -45,27 +45,28 @@ (when (instance? java.util.Map o) java.util.Map))}}))))) (testing "single method override" - (let [config {:classes {'java.lang.String - {:class java.lang.String - :instance-methods {'toString + (when-not tu/native? + (let [config {:classes {'java.lang.String + {:class java.lang.String + :instance-methods {'toString ;; REVIEW should toString also receive the class like the functions in :static-methods - (fn [_s] - :dude)}}}}] - (is (= :dude (tu/eval* "(.toString \"your name\")" config))) - (is (= 9 (tu/eval* "(.length \"your name\")" config))))) - + (fn [_s] + :dude)}}}}] + (is (= :dude (tu/eval* "(.toString \"your name\")" config))) + (is (= 9 (tu/eval* "(.length \"your name\")" config)))))) (testing "single method allowed" - (let [config {:classes {'java.lang.String - {:class java.lang.String - :instance-methods {:deny true - 'toString + (when-not tu/native? + (let [config {:classes {'java.lang.String + {:class java.lang.String + :instance-methods {:deny true + 'toString ;; REVIEW should toString also receive the class like the functions in :static-methods - (fn [_s] - :dude)}}}}] - (is (= :dude (tu/eval* "(.toString \"your name\")" config))) - (is (thrown-with-msg? Exception #"allowed" - (tu/eval* "(.length \"your name\")" config))))))) + (fn [_s] + :dude)}}}}] + (is (= :dude (tu/eval* "(.toString \"your name\")" config))) + (is (thrown-with-msg? Exception #"allowed" + (tu/eval* "(.length \"your name\")" config)))))))) #?(:clj (deftest instance-fields From 3d821af3520974f49910a4cd39824c9048b47df7 Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Tue, 10 Mar 2026 16:10:36 +0200 Subject: [PATCH 5/9] Revert "Idea: possible optimization" This reverts commit f3305b36fa4c7b3e8293cae1328b4084d8651d0b. --- src/sci/impl/analyzer.cljc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sci/impl/analyzer.cljc b/src/sci/impl/analyzer.cljc index c76e7577..9a38bb79 100644 --- a/src/sci/impl/analyzer.cljc +++ b/src/sci/impl/analyzer.cljc @@ -1024,10 +1024,10 @@ args (object-array args) class-expr (:class-expr (meta expr))] ;; prefab static-methods - (if-let [f (-> ctx :env deref :class->opts - (some-> :static-methods - (get (interop/fully-qualify-class ctx class-expr)) - (get method-expr)))] + (if-let [f (some-> ctx :env deref + :class->opts :static-methods + (get (interop/fully-qualify-class ctx class-expr)) + (get method-expr))] (return-call ctx expr f (cons instance-expr args) stack nil) (sci.impl.types/->Node (interop/invoke-static-method ctx bindings instance-expr meth-name From 798ffd24299096b0f6ec31b205ded0c913d92ac0 Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Tue, 10 Mar 2026 16:16:03 +0200 Subject: [PATCH 6/9] Revert "Add :deny option to deny any non-listed instance method" This reverts commit 0643baf64a5386fb7ca7745ebe43f8bf84eda5e1. --- src/sci/impl/evaluator.cljc | 10 +++------- test/sci/interop_test.cljc | 15 +-------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/src/sci/impl/evaluator.cljc b/src/sci/impl/evaluator.cljc index 06ed7f0f..c8846d88 100644 --- a/src/sci/impl/evaluator.cljc +++ b/src/sci/impl/evaluator.cljc @@ -161,13 +161,9 @@ :cljs (.-name instance-class)) instance-class-symbol (symbol instance-class-name)] (if-let [class-config (get class->opts instance-class-symbol)] - (if-let [instance-methods (:instance-methods class-config)] - - (if-let [f (get instance-methods (symbol method-str))] - (apply f instance-expr* args) - (if (:deny instance-methods) - (throw-error-with-location (str "Method " method-str " on " instance-class " not allowed!") instance-expr) - (normal-interop instance-class))) + (if-let [f (some-> class-config :instance-methods + (get (symbol method-str)))] + (apply f instance-expr* args) (normal-interop instance-class)) (let [^Class target-class (when-let [f (:public-class env)] (f instance-expr*))] diff --git a/test/sci/interop_test.cljc b/test/sci/interop_test.cljc index e5f29b1a..f26c3f8c 100644 --- a/test/sci/interop_test.cljc +++ b/test/sci/interop_test.cljc @@ -43,7 +43,6 @@ {:classes {'java.util.Map 'java.util.Map :public-class (fn [o] (when (instance? java.util.Map o) java.util.Map))}}))))) - (testing "single method override" (when-not tu/native? (let [config {:classes {'java.lang.String @@ -53,20 +52,8 @@ (fn [_s] :dude)}}}}] (is (= :dude (tu/eval* "(.toString \"your name\")" config))) - (is (= 9 (tu/eval* "(.length \"your name\")" config)))))) + (is (= 9 (tu/eval* "(.length \"your name\")" config)))))))) - (testing "single method allowed" - (when-not tu/native? - (let [config {:classes {'java.lang.String - {:class java.lang.String - :instance-methods {:deny true - 'toString - ;; REVIEW should toString also receive the class like the functions in :static-methods - (fn [_s] - :dude)}}}}] - (is (= :dude (tu/eval* "(.toString \"your name\")" config))) - (is (thrown-with-msg? Exception #"allowed" - (tu/eval* "(.length \"your name\")" config)))))))) #?(:clj (deftest instance-fields From a5ed38dbd38448c5aebf338907433b5e9754973b Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Tue, 10 Mar 2026 16:40:47 +0200 Subject: [PATCH 7/9] Arguments should be evaluated + test --- src/sci/impl/evaluator.cljc | 2 +- test/sci/interop_test.cljc | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/sci/impl/evaluator.cljc b/src/sci/impl/evaluator.cljc index c8846d88..8f45731a 100644 --- a/src/sci/impl/evaluator.cljc +++ b/src/sci/impl/evaluator.cljc @@ -163,7 +163,7 @@ (if-let [class-config (get class->opts instance-class-symbol)] (if-let [f (some-> class-config :instance-methods (get (symbol method-str)))] - (apply f instance-expr* args) + (apply f instance-expr* (map (fn [arg] (sci.impl.types/eval arg ctx bindings)) args)) (normal-interop instance-class)) (let [^Class target-class (when-let [f (:public-class env)] (f instance-expr*))] diff --git a/test/sci/interop_test.cljc b/test/sci/interop_test.cljc index f26c3f8c..c36328f0 100644 --- a/test/sci/interop_test.cljc +++ b/test/sci/interop_test.cljc @@ -47,12 +47,14 @@ (when-not tu/native? (let [config {:classes {'java.lang.String {:class java.lang.String - :instance-methods {'toString + :instance-methods {'lastIndexOf (fn [s needle] (.lastIndexOf s needle)) ; String/.lastIndexOf syntax only added as of 1.12 + 'toString ;; REVIEW should toString also receive the class like the functions in :static-methods (fn [_s] :dude)}}}}] (is (= :dude (tu/eval* "(.toString \"your name\")" config))) - (is (= 9 (tu/eval* "(.length \"your name\")" config)))))))) + (is (= 9 (tu/eval* "(.length \"your name\")" config))) + (is (= 5 (tu/eval* "(let [needle \"name\"] (.lastIndexOf \"your name\" needle))" config)))))))) #?(:clj From 8e9a7395262137af2b0a39d1c09f63002613d156 Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Tue, 10 Mar 2026 16:54:03 +0200 Subject: [PATCH 8/9] inline normal-interop --- src/sci/impl/evaluator.cljc | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/sci/impl/evaluator.cljc b/src/sci/impl/evaluator.cljc index 8f45731a..d694ca14 100644 --- a/src/sci/impl/evaluator.cljc +++ b/src/sci/impl/evaluator.cljc @@ -140,23 +140,20 @@ (if-not (identical? none-sentinel v) v (let [instance-class #?(:clj (or (when tag-class - (if (instance? tag-class instance-expr*) - tag-class - (class instance-expr*))) - (class instance-expr*)) + (if (instance? tag-class instance-expr*) + tag-class + (class instance-expr*))) + (class instance-expr*)) :cljs (type instance-expr*)) env @(:env ctx) class->opts (:class->opts env) allowed? (or #?(:cljs allowed) - (get class->opts :allow)) - normal-interop - (fn [target-class] - (if field-access - (interop/invoke-instance-field instance-expr* target-class method-str) - (interop/invoke-instance-method ctx bindings instance-expr* target-class method-str args arg-count arg-types)))] + (get class->opts :allow))] (if allowed? - (normal-interop instance-class) + (if field-access + (interop/invoke-instance-field instance-expr* instance-class method-str) + (interop/invoke-instance-method ctx bindings instance-expr* instance-class method-str args arg-count arg-types)) (let [instance-class-name #?(:clj (.getName ^Class instance-class) :cljs (.-name instance-class)) instance-class-symbol (symbol instance-class-name)] @@ -164,14 +161,18 @@ (if-let [f (some-> class-config :instance-methods (get (symbol method-str)))] (apply f instance-expr* (map (fn [arg] (sci.impl.types/eval arg ctx bindings)) args)) - (normal-interop instance-class)) + (if field-access + (interop/invoke-instance-field instance-expr* instance-class method-str) + (interop/invoke-instance-method ctx bindings instance-expr* instance-class method-str args arg-count arg-types))) (let [^Class target-class (when-let [f (:public-class env)] (f instance-expr*))] ;; we have to check options at run time, since we don't know what the class ;; of instance-expr is at analysis time (if target-class - (normal-interop target-class) + (if field-access + (interop/invoke-instance-field instance-expr* target-class method-str) + (interop/invoke-instance-method ctx bindings instance-expr* target-class method-str args arg-count arg-types)) (throw-error-with-location (str "Method " method-str " on " instance-class " not allowed!") instance-expr)))))))))) ;;;; End interop From bcef6a2addef5e3469eca2a9170e155726bc3476 Mon Sep 17 00:00:00 2001 From: Jeroen van Dijk Date: Tue, 10 Mar 2026 17:07:03 +0200 Subject: [PATCH 9/9] For non-native tests use sci/eval-string Instead test.util/eval* As per CLAUDE.md --- test/sci/interop_test.cljc | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/test/sci/interop_test.cljc b/test/sci/interop_test.cljc index c36328f0..af5d797b 100644 --- a/test/sci/interop_test.cljc +++ b/test/sci/interop_test.cljc @@ -44,17 +44,16 @@ :public-class (fn [o] (when (instance? java.util.Map o) java.util.Map))}}))))) (testing "single method override" - (when-not tu/native? - (let [config {:classes {'java.lang.String - {:class java.lang.String - :instance-methods {'lastIndexOf (fn [s needle] (.lastIndexOf s needle)) ; String/.lastIndexOf syntax only added as of 1.12 - 'toString + (let [config {:classes {'java.lang.String + {:class java.lang.String + :instance-methods {'lastIndexOf (fn [s needle] (.lastIndexOf s needle)) ; String/.lastIndexOf syntax only added as of 1.12 + 'toString ;; REVIEW should toString also receive the class like the functions in :static-methods - (fn [_s] - :dude)}}}}] - (is (= :dude (tu/eval* "(.toString \"your name\")" config))) - (is (= 9 (tu/eval* "(.length \"your name\")" config))) - (is (= 5 (tu/eval* "(let [needle \"name\"] (.lastIndexOf \"your name\" needle))" config)))))))) + (fn [_s] + :dude)}}}}] + (is (= :dude (sci/eval-string "(.toString \"your name\")" config))) + (is (= 9 (sci/eval-string "(.length \"your name\")" config))) + (is (= 5 (sci/eval-string "(let [needle \"name\"] (.lastIndexOf \"your name\" needle))" config))))))) #?(:clj