-
-
Save 573/f9c3bb5099b78352d48577ef342d5097 to your computer and use it in GitHub Desktop.
GraalVM polyglot interop between Clojure and JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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 | |
(.build (Context/newBuilder (into-array ["js"])))) | |
(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") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment