-
-
Save taylorwood/bb3ebfec5d5de3cccc867a9eba216c18 to your computer and use it in GitHub Desktop.
| (ns polydact.core | |
| (:import (clojure.lang IFn) | |
| (org.graalvm.polyglot Context Value) | |
| (org.graalvm.polyglot.proxy ProxyArray ProxyExecutable ProxyObject))) | |
| (set! *warn-on-reflection* true) | |
| (comment | |
| (do | |
| (def context | |
| (-> (Context/newBuilder (into-array ["js"])) | |
| (.allowHostAccess HostAccess/ALL) | |
| (.build))) | |
| (defn ^Value eval-js [code] | |
| (.eval ^Context context "js" code))) | |
| (.as (eval-js "Number.MAX_VALUE") Object) | |
| ;=> 1.7976931348623157E308 | |
| (type *1) | |
| ;=> java.lang.Double | |
| (.as (eval-js "[{}]") Object) | |
| ;=> {"0" {}} | |
| (.as (eval-js "[{}]") java.util.List) | |
| ;=> ({}) | |
| #_cool!) | |
| (defn- execute | |
| [^Value execable & args] | |
| (.execute execable (object-array args))) | |
| (declare value->clj) | |
| (defmacro ^:private reify-ifn | |
| "Convenience macro for reifying IFn for executable polyglot Values." | |
| [v] | |
| (let [invoke-arity | |
| (fn [n] | |
| (let [args (map #(symbol (str "arg" (inc %))) (range n))] | |
| (if (seq args) | |
| ;; TODO test edge case for final `invoke` arity w/varargs | |
| `(~'invoke [this# ~@args] (value->clj (execute ~v ~@args))) | |
| `(~'invoke [this#] (value->clj (execute ~v))))))] | |
| `(reify IFn | |
| ~@(map invoke-arity (range 22)) | |
| (~'applyTo [this# args#] (value->clj (apply execute ~v args#)))))) | |
| (macroexpand '(reify-ifn v)) | |
| (defn proxy-fn | |
| "Returns a ProxyExecutable instance for given function, allowing it to be | |
| invoked from polyglot contexts." | |
| [f] | |
| (reify ProxyExecutable | |
| (execute [_this args] | |
| (apply f (map value->clj args))))) | |
| (defn value->clj | |
| "Returns a Clojure (or Java) value for given polyglot Value if possible, | |
| otherwise throws." | |
| [^Value v] | |
| (cond | |
| (.isNull v) nil | |
| (.isHostObject v) (.asHostObject v) | |
| (.isBoolean v) (.asBoolean v) | |
| (.isString v) (.asString v) | |
| (.isNumber v) (.as v Number) | |
| (.canExecute v) (reify-ifn v) | |
| (.hasArrayElements v) (into [] | |
| (for [i (range (.getArraySize v))] | |
| (value->clj (.getArrayElement v i)))) | |
| (.hasMembers v) (into {} | |
| (for [k (.getMemberKeys v)] | |
| [k (value->clj (.getMember v k))])) | |
| :else (throw (Exception. "Unsupported value")))) | |
| (comment | |
| (def js->clj (comp value->clj eval-js)) | |
| (js->clj "[{}]") | |
| ;=> [{}] | |
| (js->clj "false") | |
| ;=> false | |
| (js->clj "3 / 3.33") | |
| ;=> 0.9009009009009009 | |
| (js->clj "123123123123123123123123123123123") | |
| ;=> 1.2312312312312312E32 | |
| (def doubler (js->clj "(n) => {return n * 2;}")) | |
| (doubler 2) | |
| ;=> 4 | |
| (js->clj "m = {foo: 1, bar: '2', baz: {0: false}};") | |
| ;=> {"foo" 1, "bar" "2", "baz" {"0" false}} | |
| (def factorial | |
| (eval-js " | |
| var m = []; | |
| function factorial (n) { | |
| if (n == 0 || n == 1) return 1; | |
| if (m[n] > 0) return m[n]; | |
| return m[n] = factorial(n - 1) * n; | |
| } | |
| x = {fn: factorial, memos: m};")) | |
| ((get (value->clj factorial) "fn") 12) | |
| ;=> 479001600 | |
| (get (value->clj factorial) "memos") | |
| ;=> [nil nil 2 6 24 120 720 5040 40320 362880 3628800 39916800 479001600] | |
| ((get (value->clj factorial) "fn") 24) | |
| ;=> 6.204484017332394E23 | |
| (get (value->clj factorial) "memos") | |
| ;=> [nil nil 2 6 24 120 720 5040 40320 362880 3628800 39916800 479001600 ... truncated for brevity] | |
| (eval-js "var foo = 0xFFFF") | |
| (eval-js "console.log(foo);") | |
| ;=> #object[org.graalvm.polyglot.Value 0x3f9d2028 "undefined"] | |
| ;65535 | |
| (js->clj "1 + '1'") | |
| ;=> "11" | |
| (js->clj "['foo', 10, 2].sort()") | |
| ;=> [10 2 "foo"] | |
| (def js-aset | |
| (js->clj "(arr, idx, val) => { arr[idx] = val; return arr; }")) | |
| (js-aset (ProxyArray/fromArray (object-array [1 2 3])) 1 nil) | |
| ;=> [1 nil 3] | |
| (sort [{:b nil} \a 1 "a" "A" #{\a} :foo -1 0 {:a nil} "bar"]) | |
| (def js-sort | |
| (js->clj "(...vs) => { return vs.sort(); }")) | |
| (apply js-sort [{:b nil} \a 1 "a" "A" #{\a} :foo -1 0 {:a nil} "bar"]) | |
| ;=> [-1 0 1 "A" #{\a} :foo {:a nil} {:b nil} "a" "a" "bar"] | |
| (def variadic-fn | |
| (js->clj "(x, y, ...z) => { return [x, y, z]; }")) | |
| (apply variadic-fn :foo :bar (range 3)) | |
| ;=> [:foo :bar [0 1 2]] | |
| (def ->json | |
| (js->clj "(x) => { return JSON.stringify(x); }")) | |
| (->json [1 2 3]) | |
| ;=> "[1,2,3]" | |
| (->json (ProxyObject/fromMap {"foo" 1, "bar" nil})) | |
| ;=> "{\"foo\":1,\"bar\":null}" | |
| (def json-> | |
| (js->clj "(x) => { return JSON.parse(x); }")) | |
| (json-> (->json [1 2 3])) | |
| ;=> [1 2 3] | |
| (json-> (->json (ProxyObject/fromMap {"foo" 1}))) | |
| ;=> {"foo" 1} | |
| (def json-object | |
| (js->clj "(m) => { return m.foo + m.foo; }")) | |
| (json-object (ProxyObject/fromMap {"foo" 1})) | |
| ;=> 2 | |
| (def clj-lambda | |
| (js->clj " | |
| m = {foo: [1, 2, 3], | |
| bar: { | |
| baz: ['a', 'z'] | |
| }}; | |
| (fn) => { return fn(m); } | |
| ")) | |
| (clj-lambda | |
| (proxy-fn #(clojure.walk/prewalk | |
| (fn [v] (if (and (vector? v) | |
| (not (map-entry? v))) | |
| (vec (reverse v)) | |
| v)) | |
| %))) | |
| ;=> {"foo" [3 2 1], "bar" {"baz" ["z" "a"]}} | |
| (def js-reduce | |
| (let [reduce (js->clj "(f, coll) => { return coll.reduce(f); }") | |
| reduce-init (js->clj "(f, coll, init) => { return coll.reduce(f, init); }")] | |
| (fn | |
| ([f coll] (reduce f coll)) | |
| ([f init coll] (reduce-init f coll init))))) | |
| (js-reduce + (range 10)) | |
| ;=> 45 | |
| (js-reduce + -5.5 (range 10)) | |
| ;=> 39.5 | |
| (js-reduce (fn [acc elem] | |
| (assoc acc (keyword (str elem)) (doubler elem))) | |
| {} | |
| (range 5)) | |
| ;=> {:0 0, :1 2, :2 4, :3 6, :4 8} | |
| (def log-coll | |
| (js->clj "(coll) => { for (i in coll) console.log(coll[i]); }")) | |
| (log-coll (repeatedly 3 #(do (prn 'sleeping) | |
| (Thread/sleep 100) | |
| (rand)))) | |
| (log-coll (range)) | |
| "nice") |
@kolharsam I think some Graal security stuff has changed since I wrote this. Try setting (.allowHostAccess context HostAccess/ALL). This works for me now:
(js-reduce + (range 10))
=> 45More info here https://github.com/graalvm/graaljs/blob/master/docs/user/ScriptEngine.md. Thanks!
@taylorwood, thanks for helping out! It works for me too now.
I just had a few more questions if you don't mind:
(.as (eval-js "[{}]") Object)=> I get the output for this as{}- any possible explanations?- I want to play around with this a little more. Could you point me to something interesting?
- I also want to learn more about GraalVM any place that offers the best info apart from the docs?
Thanks again, @taylorwood
Hello @taylorwood,
I have found your interesting post on GraalVM Polyglot with Clojure and tried to play with it! Unfortunately when I try to run it with lein I have the following error
Exception in thread "main" java.lang.IllegalArgumentException: Could not find option with name version., compiling:(core.clj:30:14)
which refers to this line:
(def context (.build (Context/newBuilder (into-array ["js"]))))
No idea what I am doing wrong ! I am using GraalVM 20.1, java11 on Linux... The problem arise with dependency Clojure 1.9.0 or 1.10.1...
Thanks for any advice
@taylorwood very informative, thank you!
Hey @taylorwood
Thanks for sharing this. I was trying it out and I was getting this error on the
js-reducemethod:Any help I could get from you?
I'm running
Java 8 & GraalVM 20