diff --git a/deps.edn b/deps.edn index 3466776f..62a378a2 100644 --- a/deps.edn +++ b/deps.edn @@ -1,14 +1,12 @@ {:paths ["resources" "src"] :deps {borkdude/edamame {:mvn/version "1.4.32"} - borkdude/sci.impl.reflector {:mvn/version "0.0.5"} org.babashka/sci.impl.types {:mvn/version "0.0.2"} borkdude/graal.locking {:mvn/version "0.0.2"}} :aliases {:examples {:extra-paths ["examples"]} - :dev {:extra-deps {org.clojure/clojure {:mvn/version "1.12.0"}} - :extra-paths ["reflector/src-java11"]} + :dev {:extra-deps {org.clojure/clojure {:mvn/version "1.12.0"}}} :test {:extra-paths ["test" "test-resources"] - :extra-deps {org.clojure/clojure {:mvn/version "1.9.0"} + :extra-deps {org.clojure/clojure {:mvn/version "1.10.3"} org.clojure/clojurescript {:mvn/version "1.11.132"} clj-commons/conch {:mvn/version "0.9.2"} funcool/promesa {:mvn/version "8.0.450"}}} diff --git a/project.clj b/project.clj index f3563bf9..859c6b00 100644 --- a/project.clj +++ b/project.clj @@ -9,14 +9,12 @@ :license {:name "Eclipse Public License 1.0" :url "http://opensource.org/licenses/eclipse-1.0.php"} :source-paths ["src"] - :dependencies [[org.clojure/clojure "1.9.0"] - [borkdude/sci.impl.reflector "0.0.5"] + :dependencies [[org.clojure/clojure "1.10.3"] [borkdude/edamame "1.4.32"] [org.babashka/sci.impl.types "0.0.2"] [borkdude/graal.locking "0.0.2"]] :plugins [[lein-codox "0.10.7"]] - :profiles {:clojure-1.9.0 {:dependencies [[org.clojure/clojure "1.9.0"]]} - :clojure-1.10.3 {:depdencies [[org.clojure/clojure "1.10.3"]]} + :profiles {:clojure-1.10.3 {:depdencies [[org.clojure/clojure "1.10.3"]]} :clojure-1.11.1 {:dependencies [[org.clojure/clojure "1.11.1"]]} :native-image {:dependencies [[org.clojure/clojure "1.10.3"]]} :dev {:dependencies [[thheller/shadow-cljs "2.8.64"]]} diff --git a/reflector/.gitignore b/reflector/.gitignore deleted file mode 100644 index 8b137891..00000000 --- a/reflector/.gitignore +++ /dev/null @@ -1 +0,0 @@ - diff --git a/reflector/project.clj b/reflector/project.clj deleted file mode 100644 index 278c80d1..00000000 --- a/reflector/project.clj +++ /dev/null @@ -1,10 +0,0 @@ -(defproject borkdude/sci.impl.reflector "0.0.5" - :dependencies [[org.clojure/clojure "1.9.0"]] - :description "JVM reflection support for SCI" - :licence "MIT" - :java-source-paths ["src"] - :javac-options ["-target" "1.8" "-source" "1.8" "-Xlint:-options"] - :deploy-repositories [["clojars" {:url "https://clojars.org/repo" - :username :env/clojars_user - :password :env/clojars_pass - :sign-releases false}]]) diff --git a/reflector/script/deploy b/reflector/script/deploy deleted file mode 100755 index 0bc22f8a..00000000 --- a/reflector/script/deploy +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -lein deploy clojars diff --git a/reflector/src/sci/impl/FISupport.java b/reflector/src/sci/impl/FISupport.java deleted file mode 100644 index 3fa8d052..00000000 --- a/reflector/src/sci/impl/FISupport.java +++ /dev/null @@ -1,32 +0,0 @@ -package sci.impl; - -import clojure.lang.RT; -import clojure.lang.IPersistentSet; -import java.util.concurrent.Callable; -import java.lang.reflect.Modifier; -import java.util.Comparator; - -class FISupport { - private static final IPersistentSet AFN_FIS = RT.set(Callable.class, Runnable.class, Comparator.class); - private static final IPersistentSet OBJECT_METHODS = RT.set("equals", "toString", "hashCode"); - - // Return FI method if: - // 1) Target is a functional interface and not already implemented by AFn - // 2) Target method matches one of our fn invoker methods (0 <= arity <= 10) - protected static java.lang.reflect.Method maybeFIMethod(Class target) { - if (target != null && target.isAnnotationPresent(FunctionalInterface.class) - && !AFN_FIS.contains(target)) { - - java.lang.reflect.Method[] methods = target.getMethods(); - for (java.lang.reflect.Method method : methods) { - if (method.getParameterCount() >= 0 && method.getParameterCount() <= 10 - && Modifier.isAbstract(method.getModifiers()) - && !OBJECT_METHODS.contains(method.getName())) - return method; - } - } - return null; - } -} - - diff --git a/reflector/src/sci/impl/Reflector.java b/reflector/src/sci/impl/Reflector.java deleted file mode 100644 index e18e039c..00000000 --- a/reflector/src/sci/impl/Reflector.java +++ /dev/null @@ -1,749 +0,0 @@ -/** clojure.lang.Reflector adapted for sci **/ -/** https://github.com/clojure/clojure/commits/master/src/jvm/clojure/lang/Reflector.java **/ -/** Patches made: - - Extra imports after package decl. - - Made invokeMatchingMethod public (around line 169) - - Compiler.FISupport was extracted into sci.impl.FISupport -**/ - -/** - * Copyright (c) Rich Hickey. All rights reserved. - * The use and distribution terms for this software are covered by the - * Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) - * which can be found in the file epl-v10.html at the root of this distribution. - * By using this software in any fashion, you are agreeing to be bound by - * the terms of this license. - * You must not remove this notice, or any other, from this software. - **/ - -/* rich Apr 19, 2006 */ - -package sci.impl; - -/** PATCH **/ -import clojure.lang.Util; -import clojure.lang.RT; -import clojure.lang.Compiler; -import clojure.lang.IFn; -import java.lang.reflect.Proxy; -/** END PATCH **/ - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.*; -import java.util.stream.Collectors; - -public class Reflector{ - -private static final MethodHandle CAN_ACCESS_PRED; - -// Java 8 is oldest JDK supported -private static boolean isJava8() { - return System.getProperty("java.vm.specification.version").equals("1.8"); -} - -static { - MethodHandle pred = null; - try { - if (! isJava8()) - pred = MethodHandles.lookup().findVirtual(Method.class, "canAccess", MethodType.methodType(boolean.class, Object.class)); - } catch (Throwable t) { - Util.sneakyThrow(t); - } - CAN_ACCESS_PRED = pred; -} - -private static boolean canAccess(Method m, Object target) { - if (CAN_ACCESS_PRED != null) { - // JDK9+ use j.l.r.AccessibleObject::canAccess, which respects module rules - try { - return (boolean) CAN_ACCESS_PRED.invoke(m, target); - } catch (Throwable t) { - throw Util.sneakyThrow(t); - } - } else { - // JDK 8 - return true; - } -} - -private static Collection interfaces(Class c) { - Set interfaces = new HashSet(); - Deque toWalk = new ArrayDeque(); - toWalk.addAll(Arrays.asList(c.getInterfaces())); - Class iface = toWalk.poll(); - while (iface != null) { - interfaces.add(iface); - toWalk.addAll(Arrays.asList(iface.getInterfaces())); - iface = toWalk.poll(); - } - return interfaces; -} - -private static Method tryFindMethod(Class c, Method m) { - if(c == null) return null; - try { - return c.getMethod(m.getName(), m.getParameterTypes()); - } catch(NoSuchMethodException e) { - return null; - } -} - -private static Method toAccessibleSuperMethod(Method m, Object target) { - Method selected = m; - while(selected != null) { - if(canAccess(selected, target)) return selected; - selected = tryFindMethod(selected.getDeclaringClass().getSuperclass(), m); - } - - Collection interfaces = interfaces(m.getDeclaringClass()); - for(Class c : interfaces) { - selected = tryFindMethod(c, m); - if(selected != null) return selected; - } - return null; -} - -public static Object invokeInstanceMethod(Object target, String methodName, Object[] args) { - return invokeInstanceMethodOfClass(target, target.getClass(), methodName, args); -} - -public static Object invokeInstanceMethodOfClass(Object target, Class c, String methodName, Object[] args) { - List methods = getMethods(c, args.length, methodName, false).stream() - .map(method -> toAccessibleSuperMethod(method, target)) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - return invokeMatchingMethod(methodName, methods, c, target, args); -} - -public static Object invokeInstanceMethodOfClass(Object target, String className, String methodName, Object[] args) { - return invokeInstanceMethodOfClass(target, RT.classForName(className), methodName, args); -} - -private static Throwable getCauseOrElse(Exception e) { - if (e.getCause() != null) - return e.getCause(); - return e; -} - -private static RuntimeException throwCauseOrElseException(Exception e) { - if (e.getCause() != null) - throw Util.sneakyThrow(e.getCause()); - throw Util.sneakyThrow(e); -} - -private static String noMethodReport(String methodName, Class contextClass, Object[] args){ - return "No matching method " + methodName + " found taking " + args.length + " args" - + (contextClass != null ? " for " + contextClass : ""); -} - - private static Method matchMethod(List methods, Object[] args) { - return matchMethod(methods, args, null); - } - - private static Method matchMethod(List methods, Object[] args, Class[] argTypes) { - Method foundm = null; - for(Iterator i = methods.iterator(); i.hasNext();) { - Method m = (Method) i.next(); - Class[] params = m.getParameterTypes(); - if(isCongruent(params, args, argTypes) && (foundm == null || Compiler.subsumes(params, foundm.getParameterTypes()))) { - foundm = m; - } - } - return foundm; -} - -private static Object[] widenBoxedArgs(Object[] args) { - Object[] widenedArgs = new Object[args.length]; - for(int i=0; i 0) - return invokeMatchingMethod(name, meths, target, RT.EMPTY_ARRAY); - else - return getInstanceField(target, name); - } -} - -public static Object invokeInstanceMember(Object target, String name) { - //check for field first - Class c = target.getClass(); - Field f = getField(c, name, false); - if(f != null) //field get - { - try - { - return prepRet(f.getType(), f.get(target)); - } - catch(IllegalAccessException e) - { - throw Util.sneakyThrow(e); - } - } - return invokeInstanceMethod(target, name, RT.EMPTY_ARRAY); -} - -public static Object invokeInstanceMember(String name, Object target, Object arg1) { - //check for field first - Class c = target.getClass(); - Field f = getField(c, name, false); - if(f != null) //field set - { - try - { - f.set(target, boxArg(f.getType(), arg1)); - } - catch(IllegalAccessException e) - { - throw Util.sneakyThrow(e); - } - return arg1; - } - return invokeInstanceMethod(target, name, new Object[]{arg1}); -} - -public static Object invokeInstanceMember(String name, Object target, Object... args) { - return invokeInstanceMethod(target, name, args); -} - - -static public Field getField(Class c, String name, boolean getStatics){ - Field[] allfields = c.getFields(); - for(int i = 0; i < allfields.length; i++) - { - if(name.equals(allfields[i].getName()) - && Modifier.isStatic(allfields[i].getModifiers()) == getStatics) - return allfields[i]; - } - return null; -} - -static public List getMethods(Class c, int arity, String name, boolean getStatics){ - Method[] allmethods = c.getMethods(); - ArrayList methods = new ArrayList(); - ArrayList bridgeMethods = new ArrayList(); - for(int i = 0; i < allmethods.length; i++) - { - Method method = allmethods[i]; - if(name.equals(method.getName()) - && Modifier.isStatic(method.getModifiers()) == getStatics - && method.getParameterTypes().length == arity) - { - try - { - if(method.isBridge() - && c.getMethod(method.getName(), method.getParameterTypes()) - .equals(method)) - bridgeMethods.add(method); - else - methods.add(method); - } - catch(NoSuchMethodException e) - { - } - } -// && (!method.isBridge() -// || (c == StringBuilder.class && -// c.getMethod(method.getName(), method.getParameterTypes()) -// .equals(method)))) -// { -// methods.add(allmethods[i]); -// } - } - - if(methods.isEmpty()) - methods.addAll(bridgeMethods); - - if(!getStatics && c.isInterface()) - { - allmethods = Object.class.getMethods(); - for(int i = 0; i < allmethods.length; i++) - { - if(name.equals(allmethods[i].getName()) - && Modifier.isStatic(allmethods[i].getModifiers()) == getStatics - && allmethods[i].getParameterTypes().length == arity) - { - methods.add(allmethods[i]); - } - } - } - return methods; -} - -// Return type coercions match coercions in FnInvokers for compiled invokers -private static Object coerceAdapterReturn(Object ret, Class targetType) { - if(targetType.isPrimitive()) { - switch (targetType.getName()) { - case "boolean": return RT.booleanCast(ret); - case "long": return RT.longCast(ret); - case "double": return RT.doubleCast(ret); - case "int": return RT.intCast(ret); - case "short": return RT.shortCast(ret); - case "byte": return RT.byteCast(ret); - case "float": return RT.floatCast(ret); - } - } - return ret; -} - -static Object boxArg(Class paramType, Object arg){ - if(arg instanceof IFn && FISupport.maybeFIMethod(paramType) != null && !(paramType.isInstance(arg))) - // Adapt IFn obj to targetType using dynamic proxy - return Proxy.newProxyInstance(RT.baseLoader(), - new Class[]{paramType}, - (proxy, method, methodArgs) -> { - Object ret = ((IFn) arg).applyTo(RT.seq(methodArgs)); - return coerceAdapterReturn(ret, method.getReturnType()); - }); - else if(!paramType.isPrimitive()) - return paramType.cast(arg); - else if(paramType == boolean.class) - return Boolean.class.cast(arg); - else if(paramType == char.class) - return Character.class.cast(arg); - else if(arg instanceof Number) - { - Number n = (Number) arg; - if(paramType == int.class) - return n.intValue(); - else if(paramType == float.class) - return n.floatValue(); - else if(paramType == double.class) - return n.doubleValue(); - else if(paramType == long.class) - return n.longValue(); - else if(paramType == short.class) - return n.shortValue(); - else if(paramType == byte.class) - return n.byteValue(); - } - throw new IllegalArgumentException("Unexpected param type, expected: " + paramType + - ", given: " + arg.getClass().getName()); -} - -static Object[] boxArgs(Class[] params, Object[] args){ - if(params.length == 0) - return null; - Object[] ret = new Object[params.length]; - for(int i = 0; i < params.length; i++) - { - Object arg = args[i]; - Class paramType = params[i]; - ret[i] = boxArg(paramType, arg); - } - return ret; -} - -static public boolean paramArgTypeMatch(Class paramType, Class argType){ - if(argType == null) - return !paramType.isPrimitive(); - if(paramType == argType || paramType.isAssignableFrom(argType)) - return true; - if(FISupport.maybeFIMethod(paramType) != null && IFn.class.isAssignableFrom(argType)) - return true; - if(paramType == int.class) - return argType == Integer.class - || argType == long.class - || argType == Long.class - || argType == short.class - || argType == byte.class;// || argType == FixNum.class; - else if(paramType == float.class) - return argType == Float.class - || argType == double.class; - else if(paramType == double.class) - return argType == Double.class - || argType == float.class;// || argType == DoubleNum.class; - else if(paramType == long.class) - return argType == Long.class - || argType == int.class - || argType == short.class - || argType == byte.class;// || argType == BigNum.class; - else if(paramType == char.class) - return argType == Character.class; - else if(paramType == short.class) - return argType == Short.class; - else if(paramType == byte.class) - return argType == Byte.class; - else if(paramType == boolean.class) - return argType == Boolean.class; - return false; -} - - static boolean isCongruent(Class[] params, Object[] args) { - return isCongruent(params, args, null); - } - - static boolean isCongruent(Class[] params, Object[] args, Class[] argTypes){ - boolean ret = false; - if(args == null) - return params.length == 0; - if(params.length == args.length) - { - ret = true; - for(int i = 0; ret && i < params.length; i++) - { - Class argType = null; - Object arg = args[i]; - if (argTypes != null) { - Object t = argTypes[i]; - if (t == null && arg != null) { - argType = arg.getClass(); - } else { - argType = argTypes[i]; - } - } else { - argType = (arg == null) ? null : arg.getClass(); - } - Class paramType = params[i]; - ret = paramArgTypeMatch(paramType, argType); - } - } - return ret; -} - -public static Object prepRet(Class c, Object x){ - if (!(c.isPrimitive() || c == Boolean.class)) - return x; - if(x instanceof Boolean) - return ((Boolean) x)?Boolean.TRUE:Boolean.FALSE; -// else if(x instanceof Integer) -// { -// return ((Integer)x).longValue(); -// } -// else if(x instanceof Float) -// return Double.valueOf(((Float) x).doubleValue()); - return x; -} - -} diff --git a/script/test/jvm b/script/test/jvm index 7d51eedf..c2a5dc57 100755 --- a/script/test/jvm +++ b/script/test/jvm @@ -2,9 +2,6 @@ set -eo pipefail -echo "Testing with Clojure 1.9.0" -lein with-profiles +clojure-1.9.0 test "$@" - echo "Testing with Clojure 1.10.3" lein with-profiles +clojure-1.10.3 test "$@" diff --git a/src/sci/impl/analyzer.cljc b/src/sci/impl/analyzer.cljc index d1ed3d50..f1ee26e5 100644 --- a/src/sci/impl/analyzer.cljc +++ b/src/sci/impl/analyzer.cljc @@ -21,9 +21,8 @@ [ana-macros constant? macro? rethrow-with-location-of-node set-namespace! recur special-syms]] [sci.impl.vars :as vars] - [sci.lang]) - #?(:clj (:import - [sci.impl Reflector])) + [sci.lang] + #?(:clj [sci.impl.reflector :as reflector])) #?(:cljs (:require-macros [sci.impl.analyzer :refer [gen-return-recur @@ -991,7 +990,7 @@ ;; of the same name, in which case it resolves to a ;; call to the method. (if-let [_ - (try (Reflector/getStaticField ^Class instance-expr ^String method-name) + (try (reflector/get-static-field ^Class instance-expr ^String method-name) (catch IllegalArgumentException _ nil))] (sci.impl.types/->Node (interop/get-static-field instance-expr method-name) @@ -1456,12 +1455,12 @@ f (fn [obj & args] (let [args (object-array args) arg-count (alength args) - ^java.util.List methods (interop/meth-cache ctx clazz meth arg-count #(Reflector/getMethods clazz arg-count meth false) :instance-methods)] - (Reflector/invokeMatchingMethod meth methods clazz obj args arg-types)))] + ^java.util.List methods (interop/meth-cache ctx clazz meth arg-count #(reflector/get-methods clazz arg-count meth false) :instance-methods)] + (reflector/invoke-matching-method meth methods clazz obj args arg-types)))] (sci.impl.types/->Node f stack)) - (try (Reflector/getStaticField ^Class clazz ^String meth) + (try (reflector/get-static-field ^Class clazz ^String meth) (catch IllegalArgumentException _ nil)) (sci.impl.types/->Node @@ -1469,7 +1468,7 @@ stack) :else (sci.impl.types/->Node (fn [& args] - (Reflector/invokeStaticMethod + (reflector/invoke-static-method clazz meth ^objects (into-array Object args))) stack))))) diff --git a/src/sci/impl/interop.cljc b/src/sci/impl/interop.cljc index 45ed7362..fcf892d8 100644 --- a/src/sci/impl/interop.cljc +++ b/src/sci/impl/interop.cljc @@ -1,10 +1,10 @@ (ns sci.impl.interop {:no-doc true} #?(:clj (:import - [java.lang.reflect Field Modifier] - [sci.impl Reflector])) + [java.lang.reflect Field Modifier])) (:require [sci.impl.types] - [sci.impl.utils :as utils])) + [sci.impl.utils :as utils] + #?(:clj [sci.impl.reflector :as reflector]))) ;; see https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/Reflector.java ;; see invokeStaticMethod, getStaticField, etc. @@ -45,7 +45,7 @@ :clj [[ctx bindings obj ^Class target-class method ^objects args arg-count arg-types] (let [^java.util.List methods - (meth-cache ctx target-class method arg-count #(Reflector/getMethods target-class arg-count method false) :instance-methods) + (meth-cache ctx target-class method arg-count #(reflector/get-methods target-class arg-count method false) :instance-methods) zero-args? (zero? arg-count)] (if (and zero-args? (.isEmpty ^java.util.List methods)) (invoke-instance-field obj target-class method) @@ -54,10 +54,10 @@ (aset args-array idx (sci.impl.types/eval (aget args idx) ctx bindings))) ;; Note: I also tried caching the method that invokeMatchingMethod looks up, but retrieving it from the cache was actually more expensive than just doing the invocation! ;; See getMatchingMethod in Reflector - (Reflector/invokeMatchingMethod method methods target-class obj args-array arg-types)))))])) + (reflector/invoke-matching-method method methods target-class obj args-array arg-types)))))])) (defn get-static-field [^Class class field-name-sym] - #?(:clj (Reflector/getStaticField class (str field-name-sym)) + #?(:clj (reflector/get-static-field class (str field-name-sym)) :cljs (unchecked-get class field-name-sym))) #?(:cljs @@ -88,7 +88,7 @@ #?(:clj (defn invoke-constructor #?(:clj [^Class class args] :cljs [constructor args]) - (Reflector/invokeConstructor class (object-array args)))) + (reflector/invoke-constructor class (object-array args)))) (defn invoke-static-method #?(:clj [ctx bindings ^Class class ^String method-name ^objects args len] :cljs [ctx bindings class method args]) @@ -99,10 +99,10 @@ (aset args-array idx (sci.impl.types/eval (aget args idx) ctx bindings))) ;; List methods = getMethods(c, args.length, methodName, true); ;; invokeMatchingMethod(methodName, methods, null, args) - (let [meths (meth-cache ctx class method-name len #(sci.impl.Reflector/getMethods class len method-name true) :static-methods)] + (let [meths (meth-cache ctx class method-name len #(reflector/get-methods class len method-name true) :static-methods)] ;; Note: I also tried caching the method that invokeMatchingMethod looks up, but retrieving it from the cache was actually more expensive than just doing the invocation! ;; See getMatchingMethod in Reflector - (sci.impl.Reflector/invokeMatchingMethod method-name meths nil args-array))) + (reflector/invoke-matching-method method-name meths nil args-array))) :cljs (js/Reflect.apply method class (.map args #(sci.impl.types/eval % ctx bindings))))) (defn fully-qualify-class [ctx sym] diff --git a/src/sci/impl/reflector.cljc b/src/sci/impl/reflector.cljc new file mode 100644 index 00000000..842a0fe3 --- /dev/null +++ b/src/sci/impl/reflector.cljc @@ -0,0 +1,320 @@ +; Copyright (c) Rich Hickey. All rights reserved. +; The use and distribution terms for this software are covered by the +; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) +; which can be found in the file epl-v10.html at the root of this distribution. +; By using this software in any fashion, you are agreeing to be bound by +; the terms of this license. +; You must not remove this notice, or any other, from this software. + +(ns sci.impl.reflector + "Minimal reflection support for SCI. + + JVM notes: + - mostly based on clojure.java.Reflector (transliterated by Claude Sonnet 4.5) + - made invokeMatchingMethod public via invoke-matching-method + - added arg-types to allow type hints to steer reflection resolution + - FISupport - extracted from Compiler to support functional interface adaptation" + {:no-doc true} + #?(:clj + (:import [java.lang.reflect Method Modifier Proxy] + [clojure.lang Reflector Compiler IFn RT]))) + +#?(:clj (set! *warn-on-reflection* :warn-on-boxed)) + +;; FISupport + +#?(:clj + (do + ;; AFn already implements these functional interfaces, so we don't need to adapt them + (def ^:private afn-fis + #{java.util.concurrent.Callable + java.lang.Runnable + java.util.Comparator}) + + (def ^:private object-methods + #{"equals" "toString" "hashCode"}) + + (defn- maybe-fi-method + "Return FI method if: + 1) Target is a functional interface and not already implemented by AFn + 2) Target method matches one of our fn invoker methods (0 <= arity <= 10)" + [^Class target] + (when (and target + (.isAnnotationPresent target java.lang.FunctionalInterface) + (not (contains? afn-fis target))) + (let [methods (.getMethods target)] + (some (fn [^Method method] + (when (and (>= (.getParameterCount method) 0) + (<= (.getParameterCount method) 10) + (Modifier/isAbstract (.getModifiers method)) + (not (contains? object-methods (.getName method)))) + method)) + methods)))))) + +;; Reflector functions + +#?(:clj + (defn get-methods + "Get methods matching the given class, arity, method name and static flag. + Delegates directly to clojure.lang.Reflector." + [^Class c arity ^String method-name static?] + (Reflector/getMethods c arity method-name static?))) + +#?(:clj + (defn get-static-field + "Get a static field value from a class. + Delegates directly to clojure.lang.Reflector." + [^Class c ^String field-name] + (Reflector/getStaticField c field-name))) + +#?(:clj + (defn invoke-constructor + "Invoke a constructor on a class with the given arguments. + Delegates directly to clojure.lang.Reflector." + [^Class c args] + (Reflector/invokeConstructor c args))) + +#?(:clj + (defn invoke-static-method + "Invoke a static method on a class with the given arguments. + Delegates directly to clojure.lang.Reflector." + [^Class c ^String method-name ^objects args] + (Reflector/invokeStaticMethod c method-name args))) + +#?(:clj + (defn- coerce-adapter-return + "Return type coercions match coercions in FnInvokers for compiled invokers" + [ret ^Class target-type] + (if (.isPrimitive target-type) + (case (.getName target-type) + "boolean" (RT/booleanCast ret) + "long" (RT/longCast ret) + "double" (RT/doubleCast ret) + "int" (RT/intCast ret) + "short" (RT/shortCast ret) + "byte" (RT/byteCast ret) + "float" (RT/floatCast ret) + ret) + ret))) + +#?(:clj + (defn- box-arg + "Box an argument to match the parameter type. + Handles IFn -> Functional Interface adaptation." + [^Class param-type ^Object arg] + (if (and (instance? IFn arg) + (when-let [_fi-method (maybe-fi-method param-type)] + (not (.isInstance param-type arg)))) + ;; Adapt IFn obj to targetType using dynamic proxy + (Proxy/newProxyInstance + (RT/baseLoader) + (into-array Class [param-type]) + (reify java.lang.reflect.InvocationHandler + (invoke [_ _proxy method method-args] + (let [ret (.applyTo ^IFn arg (RT/seq method-args))] + (coerce-adapter-return ret (.getReturnType ^Method method)))))) + ;; Standard boxing + (cond + (not (.isPrimitive param-type)) + (.cast param-type arg) + + (identical? param-type Boolean/TYPE) + (.cast Boolean arg) + + (identical? param-type Character/TYPE) + (.cast Character arg) + + (instance? Number arg) + (let [^Number n arg] + (cond + (identical? param-type Integer/TYPE) (.intValue n) + (identical? param-type Float/TYPE) (.floatValue n) + (identical? param-type Double/TYPE) (.doubleValue n) + (identical? param-type Long/TYPE) (.longValue n) + (identical? param-type Short/TYPE) (.shortValue n) + (identical? param-type Byte/TYPE) (.byteValue n) + :else (throw (IllegalArgumentException. + (str "Unexpected param type, expected: " param-type + ", given: " (.getName (.getClass arg))))))) + + :else + (throw (IllegalArgumentException. + (str "Unexpected param type, expected: " param-type + ", given: " (.getName (.getClass arg))))))))) + +#?(:clj + (defn- box-args + "Box all arguments to match parameter types." + [^objects params ^objects args] + (if (zero? (alength params)) + nil + (let [ret (object-array (alength params))] + (dotimes [i (alength params)] + (aset ret i (box-arg (aget params i) (aget args i)))) + ret)))) + +#?(:clj + (defn- param-arg-type-match? + "Check if a parameter type matches an argument type. + Includes support for functional interface adaptation." + [^Class param-type ^Class arg-type] + (cond + (nil? arg-type) + (not (.isPrimitive param-type)) + + (or (identical? param-type arg-type) (.isAssignableFrom param-type arg-type)) + true + + (and (maybe-fi-method param-type) (.isAssignableFrom IFn arg-type)) + true + + (identical? param-type Integer/TYPE) + (or (identical? arg-type Integer) + (identical? arg-type Long/TYPE) + (identical? arg-type Long) + (identical? arg-type Short/TYPE) + (identical? arg-type Byte/TYPE)) + + (identical? param-type Float/TYPE) + (or (identical? arg-type Float) + (identical? arg-type Double/TYPE)) + + (identical? param-type Double/TYPE) + (or (identical? arg-type Double) + (identical? arg-type Float/TYPE)) + + (identical? param-type Long/TYPE) + (or (identical? arg-type Long) + (identical? arg-type Integer/TYPE) + (identical? arg-type Short/TYPE) + (identical? arg-type Byte/TYPE)) + + (identical? param-type Character/TYPE) + (identical? arg-type Character) + + (identical? param-type Short/TYPE) + (identical? arg-type Short) + + (identical? param-type Byte/TYPE) + (identical? arg-type Byte) + + (identical? param-type Boolean/TYPE) + (identical? arg-type Boolean) + + :else + false))) + +#?(:clj + (defn- is-congruent? + "Check if parameters are congruent with arguments and arg-types." + [^objects params ^objects args ^objects arg-types] + (if (nil? args) + (zero? (alength params)) + (and (== (alength params) (alength args)) + (loop [i 0] + (if (< i (alength params)) + (let [arg (aget args i) + arg-type (if arg-types + (let [t (aget arg-types i)] + (if (and (nil? t) (some? arg)) + (.getClass ^Object arg) + t)) + (when arg (.getClass ^Object arg))) + param-type (aget params i)] + (if (param-arg-type-match? param-type arg-type) + (recur (inc i)) + false)) + true)))))) + +#?(:clj + (defn- match-method + "Find the best matching method from a list of methods given args and optional arg-types." + [^java.util.List methods args arg-types] + (let [size (.size methods)] + (loop [i 0 + found-m nil] + (if (< i size) + (if-let [^Method m (.get methods i)] + (let [params (.getParameterTypes m)] + (if (and (is-congruent? params args arg-types) + (or (nil? found-m) + (Compiler/subsumes params (.getParameterTypes ^Method found-m)))) + (recur (inc i) m) + (recur (inc i) found-m))) + found-m) + found-m))))) + +#?(:clj + (defn- widen-boxed-args! + "Widen boxed numeric arguments (e.g., Integer -> Long, Float -> Double). + Mutates args" + [^objects args] + (let [widened args] + (dotimes [i (alength args)] + (let [arg (aget args i)] + (when (some? arg) + (let [val-class (.getClass ^Object arg)] + (aset widened i + (cond + (or (identical? val-class Integer) (identical? val-class Short) (identical? val-class Byte)) + (.longValue ^Number arg) + + (identical? val-class Float) + (.doubleValue ^Number arg) + + :else + arg)))))) + widened))) + +#?(:clj + (defn invoke-matching-method + "Invoke a method matching the given name from a list of methods. + This is the core SCI-specific method that supports type hints via arg-types. + Parameters: + - method-name: String name of the method + - methods: java.util.List of Method objects (retured by getMethods) + - context-class: Class for error messages (can be nil for static methods) + - target: Object to invoke on (nil for static methods) + - args: Object array of arguments + - arg-types: Optional array of Class objects for type hints" + ([method-name methods ^Object target args] + (invoke-matching-method method-name methods + (when target (.getClass target)) + target args nil)) + ([method-name methods context-class target args] + (invoke-matching-method method-name methods context-class target args nil)) + ([method-name ^java.util.List methods context-class target ^objects args arg-types] + (if (.isEmpty methods) + (throw (IllegalArgumentException. + (str "No matching method " method-name " found taking " + (alength args) " args" + (when context-class (str " for " context-class))))) + (let [^Method m (if (== 1 (.size methods)) + (.get methods 0) + (or (match-method methods args arg-types) + ;; widen boxed args and re-try + (match-method methods (widen-boxed-args! args) arg-types)))] + (if (nil? m) + (throw (IllegalArgumentException. + (str "No matching method " method-name " found taking " + (alength args) " args" + (when context-class (str " for " context-class))))) + ;; Use Reflector's helper to find accessible version of method + (let [^Method + accessible-m (if (or (not (Modifier/isPublic + (.getModifiers (.getDeclaringClass m)))) + (and target + (not (.canAccess m target)))) + (clojure.lang.Reflector/getAsMethodOfAccessibleBase (or context-class (.getDeclaringClass m)) + m + target) + m)] + (when (nil? accessible-m) + (throw (IllegalArgumentException. + (str "Can't call public method of non-public class: " m)))) + (try + (let [ret (.invoke accessible-m target (box-args (.getParameterTypes accessible-m) args))] + (Reflector/prepRet (.getReturnType accessible-m) ret)) + (catch Exception e + (throw (clojure.lang.Util/sneakyThrow + (or (.getCause e) e)))))))))))) diff --git a/src/sci/impl/test.cljc b/src/sci/impl/test.cljc index 8ddbb0cc..d4abfc37 100644 --- a/src/sci/impl/test.cljc +++ b/src/sci/impl/test.cljc @@ -20,7 +20,6 @@ maybe-destructured rethrow-with-location-of-node set-namespace!]] [sci.impl.vars :as vars] #?(:cljs [cljs.tagged-literals :refer [JSValue]])) - #?(:clj (:import [sci.impl Reflector])) #?(:cljs (:require-macros [sci.impl.test :refer [Foo]]