-
-
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))
=> 45
More 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-reduce
method:Any help I could get from you?
I'm running
Java 8 & GraalVM 20