Skip to content

Commit 04e6bd9

Browse files
committed
CLJS-2831: Add a graaljs REPL environment
1 parent 9923fad commit 04e6bd9

9 files changed

Lines changed: 334 additions & 10 deletions

File tree

.travis.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ before_install:
1414
- sudo apt-get install -y libjavascriptcoregtk-3.0-bin
1515
- wget https://aka.ms/chakracore/cc_linux_x64_1_8_1 -O chakra-core.tar.gz
1616
- tar xvzf chakra-core.tar.gz
17-
- wget https://github.com/oracle/graal/releases/download/vm-1.0.0-rc1/graalvm-ce-1.0.0-rc1-linux-amd64.tar.gz
18-
- tar xzf graalvm-ce-1.0.0-rc1-linux-amd64.tar.gz
17+
- wget https://github.com/oracle/graal/releases/download/vm-1.0.0-rc4/graalvm-ce-1.0.0-rc4-linux-amd64.tar.gz
18+
- tar xzf graalvm-ce-1.0.0-rc4-linux-amd64.tar.gz
1919

2020
before_script:
2121
- script/bootstrap
@@ -55,7 +55,7 @@ script:
5555
- grep '0 failures, 0 errors.' test-out.txt
5656
- ./ChakraCoreFiles/bin/ch builds/out-adv/core-advanced-test.js | tee test-out.txt
5757
- grep '0 failures, 0 errors.' test-out.txt
58-
- ./graalvm-1.0.0-rc1/bin/js builds/out-adv/core-advanced-test.js | tee test-out.txt
58+
- ./graalvm-ce-1.0.0-rc4/bin/js builds/out-adv/core-advanced-test.js | tee test-out.txt
5959
- grep '0 failures, 0 errors.' test-out.txt
6060
- script/test-self-host | tee test-out.txt
6161
- grep '0 failures, 0 errors.' test-out.txt
@@ -67,3 +67,5 @@ script:
6767
- grep '0 failures, 0 errors.' test-out.txt
6868
- script/test-cli rhino | tee test-out.txt
6969
- grep '0 failures, 0 errors.' test-out.txt
70+
- PATH=`pwd`/graalvm-ce-1.0.0-rc4/bin:$PATH script/test-cli graaljs | tee test-out.txt
71+
- grep '0 failures, 0 errors.' test-out.txt
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
var global = this; // required by React
2+
3+
var graaljs_load = function(path) {
4+
var File = Java.type("java.io.File");
5+
var outputPath = (typeof CLJS_OUTPUT_DIR != "undefined" ? CLJS_OUTPUT_DIR : ".") + File.separator + path;
6+
if (typeof CLJS_DEBUG != "undefined" && CLJS_DEBUG) print("loading:" + outputPath);
7+
load(outputPath);
8+
};
9+
10+
goog.global.CLOSURE_IMPORT_SCRIPT = function(path) {
11+
graaljs_load("goog/" + path);
12+
return true;
13+
};
14+
15+
goog.global.isProvided_ = function(name) { return false; };
16+
17+
var __executors = Java.type("java.util.concurrent.Executors");
18+
var __executorService = __executors.newScheduledThreadPool(0);
19+
__executorService.setMaximumPoolSize(1);
20+
var __millis = Java.type("java.util.concurrent.TimeUnit").valueOf("MILLISECONDS");
21+
22+
var graaljs_tear_down = function() {
23+
__executorService.shutdown();
24+
}
25+
26+
function setTimerRequest(handler, delay, interval, args) {
27+
handler = handler || function() {};
28+
delay = delay || 0;
29+
interval = interval || 0;
30+
var voidType = Java.type("java.lang.Void").TYPE;
31+
var applyHandler = __executors.callable(function() { handler.apply(this, args); }, voidType);
32+
if (interval > 0) {
33+
return __executorService.scheduleWithFixedDelay(applyHandler, delay, interval, __millis);
34+
} else {
35+
return __executorService.schedule(applyHandler, delay, __millis);
36+
};
37+
}
38+
39+
function clearTimerRequest(future) {
40+
future.cancel(false);
41+
}
42+
43+
function setInterval() {
44+
var args = Array.prototype.slice.call(arguments);
45+
var handler = args.shift();
46+
var ms = args.shift();
47+
return setTimerRequest(handler, ms, ms, args);
48+
}
49+
50+
function clearInterval(future) {
51+
clearTimerRequest(future);
52+
}
53+
54+
function setTimeout() {
55+
var args = Array.prototype.slice.call(arguments);
56+
var handler = args.shift();
57+
var ms = args.shift();
58+
59+
return setTimerRequest(handler, ms, 0, args);
60+
}
61+
62+
function clearTimeout(future) {
63+
clearTimerRequest(future);
64+
}
65+
66+
function setImmediate() {
67+
var args = Array.prototype.slice.call(arguments);
68+
var handler = args.shift();
69+
70+
return setTimerRequest(handler, 0, 0, args);
71+
}
72+
73+
function clearImmediate(future) {
74+
clearTimerRequest(future);
75+
}

src/main/cljs/cljs/core.cljs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11525,7 +11525,8 @@ reduces them without incurring seq initialization"
1152511525
(exists? js/console)
1152611526
(enable-console-print!)
1152711527

11528-
(identical? *target* "nashorn")
11528+
(or (identical? *target* "nashorn")
11529+
(identical? *target* "graaljs"))
1152911530
(let [system (.type js/Java "java.lang.System")]
1153011531
(set! *print-newline* false)
1153111532
(set-print-fn!

src/main/cljs/cljs/stacktrace.cljc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,30 @@ goog.events.getProxy/f<@http://localhost:9000/out/goog/events/events.js:276:16"
471471
(remove nil?)
472472
vec)))
473473

474+
;; -----------------------------------------------------------------------------
475+
;; Graal.JS Stacktrace
476+
477+
(defmethod parse-stacktrace :graaljs
478+
[repl-env st err {:keys [output-dir] :as opts}]
479+
(letfn [(process-frame [frame-str]
480+
(when-not (string/blank? frame-str)
481+
(let [[function file-and-line] (string/split frame-str #"\(")
482+
[file-part line-part] (string/split file-and-line #":")]
483+
{:file (string/replace file-part
484+
(str output-dir
485+
#?(:clj File/separator :cljs "/"))
486+
"")
487+
:function function
488+
:line (when (and line-part (not (string/blank? line-part)))
489+
(parse-int
490+
(.substring line-part 0
491+
(dec (count line-part)))))
492+
:column 0})))]
493+
(->> (string/split st #"\n")
494+
(map process-frame)
495+
(remove nil?)
496+
vec)))
497+
474498
(comment
475499
(parse-stacktrace {}
476500
"Error: 1 is not ISeqable

src/main/clojure/cljs/cli.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ present"
530530
{["-re" "--repl-env"]
531531
{:arg "env"
532532
:doc (str "The REPL environment to use. Built-in "
533-
"supported values: nashorn, node, browser, "
533+
"supported values: nashorn, graaljs, node, browser, "
534534
"rhino. Defaults to browser")}}}
535535
::main {:desc "init options only for --main and --repl"}
536536
::compile {:desc "init options only for --compile"}}
@@ -569,7 +569,7 @@ present"
569569
:doc
570570
(str "The JavaScript target. Configures environment bootstrap and "
571571
"defaults to browser. Supported values: node or nodejs, nashorn, "
572-
"webworker, none") }
572+
"graaljs, webworker, none") }
573573
["-ro" "--repl-opts"] {:group ::main&compile :fn repl-env-opts-opt
574574
:arg "edn"
575575
:doc (str "Options to configure the repl-env, can be an EDN string or "

src/main/clojure/cljs/closure.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,7 +1635,7 @@
16351635
(util/output-directory opts))
16361636
closure-defines (json/write-str (:closure-defines opts))]
16371637
(case (:target opts)
1638-
:nashorn
1638+
(:nashorn :graaljs)
16391639
(output-one-file
16401640
(merge opts
16411641
(when module
@@ -1645,7 +1645,7 @@
16451645
(str "var CLJS_OUTPUT_DIR = \"" asset-path "\";\n"
16461646
"load((new java.io.File(new java.io.File(\"" asset-path "\",\"goog\"), \"base.js\")).getPath());\n"
16471647
"load((new java.io.File(new java.io.File(\"" asset-path "\",\"goog\"), \"deps.js\")).getPath());\n"
1648-
"load((new java.io.File(new java.io.File(new java.io.File(\"" asset-path "\",\"goog\"),\"bootstrap\"),\"nashorn.js\")).getPath());\n"
1648+
"load((new java.io.File(new java.io.File(new java.io.File(\"" asset-path "\",\"goog\"),\"bootstrap\"),\"" (name (:target opts)) ".js\")).getPath());\n"
16491649
"load((new java.io.File(\"" asset-path "\",\"cljs_deps.js\")).getPath());\n"
16501650
"goog.global.CLOSURE_UNCOMPILED_DEFINES = " closure-defines ";\n"
16511651
(apply str (preloads (:preloads opts)))))
@@ -2794,7 +2794,7 @@
27942794
opts))
27952795

27962796
(defn output-bootstrap [{:keys [target] :as opts}]
2797-
(when (and (#{:nodejs :nashorn} target)
2797+
(when (and (#{:nodejs :nashorn :graaljs} target)
27982798
(not= (:optimizations opts) :whitespace))
27992799
(let [target-str (name target)
28002800
outfile (io/file (util/output-directory opts)
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
;; Copyright (c) Rich Hickey. All rights reserved.
2+
;; The use and distribution terms for this software are covered by the
3+
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
4+
;; which can be found in the file epl-v10.html at the root of this distribution.
5+
;; By using this software in any fashion, you are agreeing to be bound by
6+
;; the terms of this license.
7+
;; You must not remove this notice, or any other, from this software.
8+
9+
(ns cljs.repl.graaljs
10+
(:require [clojure.java.io :as io]
11+
[clojure.string :as string]
12+
[clojure.stacktrace]
13+
[clojure.data.json :as json]
14+
[cljs.analyzer :as ana]
15+
[cljs.env :as env]
16+
[cljs.util :as util]
17+
[cljs.repl :as repl]
18+
[cljs.cli :as cli]
19+
[cljs.compiler :as comp]
20+
[cljs.closure :as closure]
21+
[cljs.stacktrace :as st])
22+
(:import [javax.script ScriptEngine ScriptException]))
23+
24+
(defn create-engine []
25+
;; In order to support AOT compilation by JVMs that don't have
26+
;; GraalVM available, we load and execute engine creation code
27+
;; here at runtime.
28+
(import '(com.oracle.truffle.js.scriptengine GraalJSScriptEngine))
29+
(import '(org.graalvm.polyglot Context))
30+
(let [engine (eval '(GraalJSScriptEngine/create nil
31+
(-> (Context/newBuilder (make-array String 0))
32+
(.allowHostAccess true)
33+
(.allowCreateThread true)
34+
(.allowNativeAccess true))))
35+
context (.getContext engine)]
36+
(.setWriter context *out*)
37+
(.setErrorWriter context *err*)
38+
engine))
39+
40+
(defn eval-str [^ScriptEngine engine ^String s]
41+
(.eval engine s))
42+
43+
(defn eval-resource
44+
"Evaluate a file on the classpath in the engine."
45+
[engine path debug]
46+
(let [r (io/resource path)]
47+
(eval-str engine (slurp r))
48+
(when debug (println "loaded: " path))))
49+
50+
(defn init-engine [engine {:keys [output-dir] :as opts} debug]
51+
(eval-str engine (format "var CLJS_DEBUG = %s;" (boolean debug)))
52+
(eval-str engine (format "var CLJS_OUTPUT_DIR = \"%s\";" output-dir))
53+
(eval-resource engine "goog/base.js" debug)
54+
(eval-resource engine "goog/deps.js" debug)
55+
(eval-resource engine "cljs/bootstrap_graaljs.js" debug)
56+
(eval-str engine
57+
(format "goog.global.CLOSURE_UNCOMPILED_DEFINES = %s;"
58+
(json/write-str (:closure-defines opts))))
59+
engine)
60+
61+
(defn tear-down-engine [engine]
62+
(eval-str engine "graaljs_tear_down();"))
63+
64+
(defn load-js-file [engine file]
65+
(eval-str engine (format "graaljs_load(\"%s\");" file)))
66+
67+
;; Create a minimal build of ClojureScript from the core library.
68+
;; Copied from clj.cljs.repl.node.
69+
(defn bootstrap-repl [engine output-dir opts]
70+
(env/ensure
71+
(let [deps-file ".graaljs_repl_deps.js"
72+
core (io/resource "cljs/core.cljs")
73+
core-js (closure/compile core
74+
(assoc opts :output-file
75+
(closure/src-file->target-file
76+
core (dissoc opts :output-dir))))
77+
deps (closure/add-dependencies opts core-js)]
78+
;; output unoptimized code and the deps file
79+
;; for all compiled namespaces
80+
(apply closure/output-unoptimized
81+
(assoc opts :output-to (.getPath (io/file output-dir deps-file)))
82+
deps)
83+
;; load the deps file so we can goog.require cljs.core etc.
84+
(load-js-file engine deps-file))))
85+
86+
(defn load-ns [engine ns]
87+
(eval-str engine
88+
(format "goog.require(\"%s\");" (comp/munge (first ns)))))
89+
90+
(def repl-filename "<cljs repl>")
91+
92+
(def ^:private skip-types #{"com.oracle.truffle.api.interop.java.TruffleMap"
93+
"com.oracle.truffle.api.interop.java.TruffleMap$FunctionTruffleMap"})
94+
95+
(defn- safe-to-string
96+
"A safe version that avoids calling .toString on types known to cause stack overflow.
97+
Also has a guard to return an unreadable containing the type if this is encountered."
98+
[x]
99+
(let [type-str (pr-str (type x))]
100+
(try
101+
(if (contains? skip-types type-str)
102+
(str #"<" type-str ">")
103+
(.toString x))
104+
(catch StackOverflowError _
105+
(str "#<stackoverflow " type-str ">")))))
106+
107+
(defrecord GraalJSEnv [engine debug]
108+
repl/IReplEnvOptions
109+
(-repl-options [this]
110+
{:output-dir ".cljs_graaljs_repl"
111+
:target :graaljs})
112+
repl/IJavaScriptEnv
113+
(-setup [this {:keys [output-dir bootstrap output-to] :as opts}]
114+
(init-engine engine opts debug)
115+
(let [env (ana/empty-env)]
116+
(if output-to
117+
(load-js-file engine output-to)
118+
(bootstrap-repl engine output-dir opts))
119+
(repl/evaluate-form this env repl-filename
120+
'(.require js/goog "cljs.core"))
121+
;; monkey-patch goog.isProvided_ to suppress useless errors
122+
(repl/evaluate-form this env repl-filename
123+
'(set! js/goog.isProvided_ (fn [ns] false)))
124+
;; monkey-patch goog.require to be more sensible
125+
(repl/evaluate-form this env repl-filename
126+
'(do
127+
(set! *loaded-libs* #{"cljs.core"})
128+
(set! (.-require js/goog)
129+
(fn [name reload]
130+
(when (or (not (contains? *loaded-libs* name)) reload)
131+
(set! *loaded-libs* (conj (or *loaded-libs* #{}) name))
132+
(js/CLOSURE_IMPORT_SCRIPT
133+
(if (some? goog/debugLoader_)
134+
(.getPathFromDeps_ goog/debugLoader_ name)
135+
(goog.object/get (.. js/goog -dependencies_ -nameToPath) name))))))))))
136+
(-evaluate [{engine :engine :as this} filename line js]
137+
(when debug (println "Evaluating: " js))
138+
(try
139+
{:status :success
140+
:value (if-let [r (eval-str engine js)] (safe-to-string r) "")}
141+
(catch ScriptException e
142+
(let [^Throwable root-cause (clojure.stacktrace/root-cause e)]
143+
{:status :exception
144+
:value (.getMessage root-cause)
145+
:stacktrace
146+
(apply str
147+
(interpose "\n"
148+
(map #(subs % 5)
149+
(filter #(clojure.string/starts-with? % "<js>.")
150+
(map str
151+
(.getStackTrace root-cause))))))}))
152+
(catch Throwable e
153+
(let [^Throwable root-cause (clojure.stacktrace/root-cause e)]
154+
{:status :exception
155+
:value (.getMessage root-cause)
156+
:stacktrace
157+
(apply str
158+
(interpose "\n"
159+
(map str
160+
(.getStackTrace root-cause))))}))))
161+
(-load [{engine :engine :as this} ns url]
162+
(load-ns engine ns))
163+
(-tear-down [this]
164+
(tear-down-engine engine))
165+
repl/IParseStacktrace
166+
(-parse-stacktrace [this frames-str ret opts]
167+
(st/parse-stacktrace this frames-str
168+
(assoc ret :ua-product :graaljs) opts))
169+
repl/IParseError
170+
(-parse-error [_ err _]
171+
(update-in err [:stacktrace]
172+
(fn [st]
173+
(string/join "\n" (drop 1 (string/split st #"\n")))))))
174+
175+
(defn repl-env* [{:keys [debug] :as opts}]
176+
(let [engine (create-engine)]
177+
(merge
178+
(GraalJSEnv. engine debug)
179+
opts)))
180+
181+
(defn repl-env
182+
"Create a Graal.JS repl-env for use with the repl/repl* method in ClojureScript."
183+
[& {:as opts}]
184+
(repl-env* opts))
185+
186+
;; -------------------------------------------------------------------------
187+
;; Command Line Support
188+
189+
(defn -main [& args]
190+
(apply cli/main repl-env args))

0 commit comments

Comments
 (0)