[250112] clojurescript compiler initial commit in clojure
Table of Contents
클로저스크립트가 어떻게 자바크립트로 변환하는지에 대해서 궁금해왔다. 당연히 뭐 AST를 만들고, 그걸 자바스크립트 코드로 변환(emit) 하겠지만 실제 구현을 보면서 어떻게 하는지 알아보자.
모든 소스코드는 cljs 의 소스코드를 참고하였다. 사실 그대로 베꼈다.
1 analyze
- analyze 함수는 AST를 만드는 함수이다.
- AST를 만들때는 :op, :env, :form, :children 이 4가지가 필수적으로 들어가는 것 같다.
- analyze 함수는 env,form,name을 받아서 AST를 만들어준다.
- env는 현재 환경을 의미한다. (namespace, locals, context)
- form은 소스코드를 의미한다.
- name은 이름을 의미한다. (javascript에서는 변수명이 될 것이다.)
op의 경우가 oprater인데 예약어냐 아니냐에 따라서 다르게 처리한다.
- op가 예약어라면 parse 함수를 이용해서 AST를 만들어준다.
- op가 예약어가 아니라면 parse-invoke 함수를 이용해서 AST를 만들어준다.
2 parse
parse를 이용해서 AST를 만들 것이다. 클로저스크립트 또한 LISP 계열 이기 때문에 리스트의 맨 첫번째 요소가 operator가 된다. 그래서 operator를 기준으로 multi-method를 만들어서 AST를 만들어보자.
(defmulti parse (fn [op & rest] op)) (defmethod parse 'if) (defmethod parse 'def) (defmethod parse 'fn*) (defmethod parse 'do) (defmethod parse 'let*) (defmethod parse 'loop) (defmethod parse 'recur) (defmethod parse 'new) (defmethod parse 'set!) (defmethod parse 'ns)
하나씩 만들어보자.
2.1 if
(defmethod parse 'if
[op env [_ test then else :as form] name]
(let [test-expr (disallowing-recur (analyze (assoc env :context :expr) test))
then-expr (analyze env then)
else-expr (analyze env else)]
{:env env
:op :if
:form form
:test test-expr
:then then-expr
:else else-expr
:children [test-expr then-expr else-expr]}))
- if문에서는 recur 사용을 막기 위해서 disallowing-recur 함수를 사용한다.
- test, then, else를 analyze 함수를 이용해서 AST로 만들어준다.
예시
(clojure.pprint/pprint (analyze {:ns 'cljs.core :locals {}} '(if true 1 2)))
{:env {:ns cljs.core, :locals {}},
:op :if,
:form (if true 1 2),
:test
{:op :constant,
:env {:ns cljs.core, :locals {}, :context :expr},
:form true},
:then {:op :constant, :env {:ns cljs.core, :locals {}}, :form 1},
:else {:op :constant, :env {:ns cljs.core, :locals {}}, :form 2},
:children
[{:op :constant,
:env {:ns cljs.core, :locals {}, :context :expr},
:form true}
{:op :constant, :env {:ns cljs.core, :locals {}}, :form 1}
{:op :constant, :env {:ns cljs.core, :locals {}}, :form 2}]}
- :constant 는 상수를 의미하고, analyze 함수에서 리턴한다.
- :op, :test, :then, :else 가 주요 key이다.
2.2 def
- def는 변수를 만드는 것이다.
- 변수를 만들때는 이름, 초기값(optional), 설명(optional)이 필요하다.
- symbol을 만들때는 resolve-var 함수를 이용해서 symbol을 만들어준다.
- init이 있으면 analyze 함수를 이용해서 AST를 만들어준다.
- 리턴값은 {:env, :op, :form, :name, :doc, :init, :children} 이다.
예시
(clojure.pprint/pprint (analyze {:ns 'cljs.core :locals {}} '(def a 1)))
{:env {:ns cljs.core, :locals {}},
:op :def,
:form (def a 1),
:name cljs.core.a,
:doc nil,
:init
{:op :constant,
:env {:ns cljs.core, :locals {}, :context :expr},
:form 1},
:children
[{:op :constant,
:env {:ns cljs.core, :locals {}, :context :expr},
:form 1}]}
2.3 fn*
- fn*은 함수를 만드는 것이다.
- 함수를 만들때는 이름, 파라미터, body가 필요하다.
- 리턴값은 {:env, :op, :name, :meths, :params, :recurs, :children} 이다.
- 함수 이름 처리
;; (fn* name? [params*] exprs*) (fn* foo [x] x) ; name = foo (fn* [x] x) ; name = nil
- 메서드 처리
;; 단일 메서드를 리스트로 변환 (fn* [x] x) ; => (fn* ([x] x))
- 파라미터 처리:
;; &를 제거 (fn* [x & y] x) ; => [x y]
- 로컬 스코프 설정:
;; 로컬 변수 설정
(fn* [x] x) ; => {:locals {x {:name x}}}
- 재귀 처리
{:names [x y] ; recur 대상 파라미터들
:flag (atom nil)} ; recur 사용 여부
- body 처리
;; return 컨텍스트로 분석 (analyze-block (assoc env :context :return) body)
2.4 do
(defmethod parse 'do
[op env [_ & exprs] _]
(merge {:env env :op :do} (analyze-block env exprs)))
- do는 여러개의 표현식을 실행하는 것이기 때문에 analyze-block 함수를 이용해서 AST를 만들어준다.
2.5 let*
(defmethod parse 'let* [op encl-env form _] (analyze-let encl-env form false))
- let*은 analyze-let 함수를 이용해서 AST를 만들어준다.
- 소스코드에 있는 bes 는 binding expressions 를 의미한다.
- env 는 덮어져있는 env에 {:context :expr} 를 추가한다. 이유는 모든 초기값은 expression 으로서 결과값을 생성해야 하기 때문이다.
실행해보자.
(clojure.pprint/pprint
(analyze-let {} '(let* [a 1] ) false))
{:env {},
:op :let,
:loop false,
:bindings
[{:name a__11765,
:init {:op :constant, :env {:context :expr}, :form 1}}],
:statements nil,
:ret
{:op :constant,
:env
{:context nil,
:locals
{a
{:name a__11765,
:init {:op :constant, :env {:context :expr}, :form 1}}}},
:form nil},
:form (let* [a 1]),
:children
[[{:op :constant,
:env
{:context nil,
:locals
{a
{:name a__11765,
:init {:op :constant, :env {:context :expr}, :form 1}}}},
:form nil}]
{:op :constant, :env {:context :expr}, :form 1}]}
좀더 많은 바인딩과 표현식을 넣어보자. 조금만 추가해도 엄청나게 산출물이 커지는데 :init, :env 같은 값이 중복되어서 나오는 것을 볼 수 있다.
중요한 것은 :bindings 에 바인딩된 값들이 들어가고, :ret에 마지막 표현식이 들어간다.
2.6 loop
loop 는 let 과 비슷하다. 다만 recur를 사용할 수 있다.
2.7 recur
*recur-frame*이 true인 경우만 recur를 사용할 수 있다.- recur 안에 인자들은 표현식으로 미리 평가되도록 하기 위해
(assoc env :context :expr)로 expression 임을 표시하고 analyze 함수를 이용해서 AST를 만들어준다. - recur 안에서는 recur가 중복으로 들어오면 안되기 때문에
(disallowing-recur ,,,)로 recur를 막는다.
2.8 new
new는 자바스크립트의 new와 같다.
:ctor, :args 를 함께 리턴하는데 나중에 emit 에서 생성자 함수로 만들 때 사용할 것이다.
2.9 set!
이것은 변수를 변경하는 것 같은데 clojure 에서 사용하는 함수임.
아직 set! 은 제대로 구현이 안되었을 확률이 높음.
이는 atom을 사용해서 값을 변경하지 않고, 그냥 javascript의 변수 변경과 같이 구현되는 것 같다.
나중에 emit 으로 알 수 있다.
3 emit
AST 를 만드는 것은 중요한 작업이지만, AST는 중간단계일 뿐이다. AST 로 무엇을 하는지가 중요하다. 이것으로 최적화를 할 수도 있고, 다른 언어로 변환할 수도 있다.
emit은 AST를 받아서 자바스크립트 코드로 변환하는 함수이다. 아주 중요한 로직이다.
(def bootjs "
cljs = {}
cljs.user = {}
cljs.lang = {}
cljs.lang.truth = function(x){return x != null && x !== false;}
cljs.lang.fnOf = function(f){return (f instanceof Function ? f : f.cljs$lang$Fn$invoke);}")
(defmulti emit-constant class)
(defmethod emit-constant nil [x] (print "null"))
(defmethod emit-constant Long [x] (print x))
(defmethod emit-constant Double [x] (print x))
(defmethod emit-constant String [x] (pr x))
(defmethod emit-constant Boolean [x] (print (if x "true" "false")))
(defmulti emit :op)
(defn emits [expr]
(with-out-str (emit expr)))
(defn emit-block
[context statements ret]
(if statements
(let [body (str "\t" (apply str (interpose "\t" (map emits statements)))
"\t" (emits ret))]
(print body))
(emit ret)))
(defmacro emit-wrap [env & body]
`(let [env# ~env]
(when (= :return (:context env#)) (print "return "))
~@body
(when-not (= :expr (:context env#)) (print ";\n"))))
(defmethod emit :var
[{:keys [info env] :as arg}]
(emit-wrap env (print (:name info))))
(defmethod emit :constant
[{:keys [form env]}]
(emit-wrap env (emit-constant form)))
(defmethod emit :if
[{:keys [test then else env]}]
(let [context (:context env)]
(if (= :expr context)
(print (str "(cljs.lang.truth(" (emits test) ")?" (emits then) ":" (emits else) ")"))
(print (str "if(cljs.lang.truth(" (emits test) "))\n\t" (emits then) " else\n\t" (emits else) "\n")))))
(defmethod emit :def
[{:keys [name init env]}]
(when init
(print name)
(print (str " = " (emits init)))
(when-not (= :expr (:context env)) (print ";\n"))))
(defmethod emit :fn
[{:keys [name params statements ret env recurs]}]
;;fn statements get erased, serve no purpose and can pollute scope if named
(when-not (= :statement (:context env))
(emit-wrap env
(print (str "(function " name "(" (apply str (interpose "," params)) "){\n"))
(when recurs (print "while(true){\n"))
(emit-block :return statements ret)
(when recurs (print "break;\n}\n"))
(print "})"))))
(defmethod emit :do
[{:keys [statements ret env]}]
(let [context (:context env)]
(when (and statements (= :expr context)) (print "(function ()"))
(when statements (print "{\n"))
(emit-block context statements ret)
(when statements (print "}"))
(when (and statements (= :expr context)) (print ")()"))))
(defmethod emit :let
[{:keys [bindings statements ret env loop]}]
(let [context (:context env)
bs (map (fn [{:keys [name init]}]
(str "var " name " = " (emits init) ";\n"))
bindings)]
(when (= :expr context) (print "(function ()"))
(print (str "{\n" (apply str bs) "\n"))
(when loop (print "while(true){\n"))
(emit-block (if (= :expr context) :return context) statements ret)
(when loop (print "break;\n}\n"))
(print "}")
(when (= :expr context) (print ")()"))))
(defmethod emit :recur
[{:keys [frame exprs env]}]
(let [temps (vec (take (count exprs) (repeatedly gensym)))
names (:names frame)]
(print "{\n")
(dotimes [i (count exprs)]
(print (str "var " (temps i) " = " (emits (exprs i)) ";\n")))
(dotimes [i (count exprs)]
(print (str (names i) " = " (temps i) ";\n")))
(print "continue;\n")
(print "}\n")))
(defmethod emit :invoke
[{:keys [f args env]}]
(emit-wrap env
(print (str "cljs.lang.fnOf(" (emits f) ")("
(apply str (interpose "," (map emits args)))
")"))))
(defmethod emit :new
[{:keys [ctor args env]}]
(emit-wrap env
(print (str "new " (emits ctor) "("
(apply str (interpose "," (map emits args)))
")"))))
(defmethod emit :set!
[{:keys [target val env]}]
(emit-wrap env (print (str (emits target) " = "(emits val)))))
(defmethod emit :ns
[{:keys [name requires macros env]}]
(println (str "//goog.provide('" name "');"))
(doseq [lib (vals requires)]
(println (str "//goog.require('" lib "');"))))
AST 로 만들어진 값을 이용해서 자바스크립트 코드로 변환한다.
4 Source Code All
(ns clojure.cljs-01)
; 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 clojure.cljs
)
(defonce namespaces (atom {}))
(def bootjs "
cljs = {}
cljs.user = {}
cljs.lang = {}
cljs.lang.truth = function(x){return x != null && x !== false;}
cljs.lang.fnOf = function(f){return (f instanceof Function?f:f.cljs$lang$Fn$invoke);}")
(defn- resolve-var [env sym]
(let [s (str sym)
lb (-> env :locals sym)
nm
(cond
lb (:name lb)
;;todo - resolve ns aliases when we have them
(namespace sym)
(symbol (str (namespace sym) "." (name sym)))
(.contains s ".")
(let [idx (.indexOf s ".")
prefix (symbol (subs s 0 idx))
suffix (subs s idx)
lb (-> env :locals prefix)]
(if lb
(symbol (str (:name lb) suffix))
sym))
:else
(symbol (str (:ns env) "." (name sym))))]
{:name nm}))
(defmulti emit-constant class)
(defmethod emit-constant nil [x] (print "null"))
(defmethod emit-constant Long [x] (print x))
(defmethod emit-constant Double [x] (print x))
(defmethod emit-constant String [x] (pr x))
(defmethod emit-constant Boolean [x] (print (if x "true" "false")))
(defmulti emit :op)
(defn emits [expr]
(with-out-str (emit expr)))
(defn emit-block
[context statements ret]
(if statements
(let [body (str "\t" (apply str (interpose "\t" (map emits statements)))
"\t" (emits ret))]
(print body))
(emit ret)))
(defmacro emit-wrap [env & body]
`(let [env# ~env]
(when (= :return (:context env#)) (print "return "))
~@body
(when-not (= :expr (:context env#)) (print ";\n"))))
(defmethod emit :var
[{:keys [info env] :as arg}]
(emit-wrap env (print (:name info))))
(defmethod emit :constant
[{:keys [form env]}]
(emit-wrap env (emit-constant form)))
(defmethod emit :if
[{:keys [test then else env]}]
(let [context (:context env)]
(if (= :expr context)
(print (str "(cljs.lang.truth(" (emits test) ")?" (emits then) ":" (emits else) ")"))
(print (str "if(cljs.lang.truth(" (emits test) "))\n\t" (emits then) " else\n\t" (emits else) "\n")))))
(defmethod emit :def
[{:keys [name init env]}]
(when init
(print name)
(print (str " = " (emits init)))
(when-not (= :expr (:context env)) (print ";\n"))))
(defmethod emit :fn
[{:keys [name params statements ret env recurs]}]
;;fn statements get erased, serve no purpose and can pollute scope if named
(when-not (= :statement (:context env))
(emit-wrap env
(print (str "(function " name "(" (apply str (interpose "," params)) "){\n"))
(when recurs (print "while(true){\n"))
(emit-block :return statements ret)
(when recurs (print "break;\n}\n"))
(print "})"))))
(defmethod emit :do
[{:keys [statements ret env]}]
(let [context (:context env)]
(when (and statements (= :expr context)) (print "(function ()"))
(when statements (print "{\n"))
(emit-block context statements ret)
(when statements (print "}"))
(when (and statements (= :expr context)) (print ")()"))))
(defmethod emit :let
[{:keys [bindings statements ret env loop]}]
(let [context (:context env)
bs (map (fn [{:keys [name init]}]
(str "var " name " = " (emits init) ";\n"))
bindings)]
(when (= :expr context) (print "(function ()"))
(print (str "{\n" (apply str bs) "\n"))
(when loop (print "while(true){\n"))
(emit-block (if (= :expr context) :return context) statements ret)
(when loop (print "break;\n}\n"))
(print "}")
(when (= :expr context) (print ")()"))))
(defmethod emit :recur
[{:keys [frame exprs env]}]
(let [temps (vec (take (count exprs) (repeatedly gensym)))
names (:names frame)]
(print "{\n")
(dotimes [i (count exprs)]
(print (str "var " (temps i) " = " (emits (exprs i)) ";\n")))
(dotimes [i (count exprs)]
(print (str (names i) " = " (temps i) ";\n")))
(print "continue;\n")
(print "}\n")))
(defmethod emit :invoke
[{:keys [f args env]}]
(emit-wrap env
(print (str "cljs.lang.fnOf(" (emits f) ")("
(apply str (interpose "," (map emits args)))
")"))))
(defmethod emit :new
[{:keys [ctor args env]}]
(emit-wrap env
(print (str "new " (emits ctor) "("
(apply str (interpose "," (map emits args)))
")"))))
(defmethod emit :set!
[{:keys [target val env]}]
(emit-wrap env (print (str (emits target) " = "(emits val)))))
(defmethod emit :ns
[{:keys [name requires macros env]}]
(println (str "//goog.provide('" name "');"))
(doseq [lib (vals requires)]
(println (str "//goog.require('" lib "');"))))
(declare analyze analyze-symbol)
(def specials '#{if def fn* do let* loop recur new set! ns})
(def ^:dynamic *recur-frame* nil)
(defmacro disallowing-recur [& body]
`(binding [*recur-frame* nil] ~@body))
(defn analyze-block
"returns {:statements .. :ret .. :children ..}"
[env exprs]
(let [statements (disallowing-recur
(seq (map #(analyze (assoc env :context :statement) %) (butlast exprs))))
ret (if (<= (count exprs) 1)
(analyze env (first exprs))
(analyze (assoc env :context (if (= :statement (:context env)) :statement :return)) (last exprs)))]
{:statements statements :ret ret :children (vec (cons ret statements))}))
(defmulti parse (fn [op & rest] op))
(defmethod parse 'if
[op env [_ test then else :as form] name]
(let [test-expr (disallowing-recur (analyze (assoc env :context :expr) test))
then-expr (analyze env then)
else-expr (analyze env else)]
{:env env :op :if :form form
:test test-expr :then then-expr :else else-expr
:children [test-expr then-expr else-expr]}))
(defmethod parse 'def
[op env form name]
(let [pfn (fn ([_ sym] {:sym sym})
([_ sym init] {:sym sym :init init})
([_ sym doc init] {:sym sym :doc doc :init init}))
args (apply pfn form)
sym (:sym args)]
(assert (not (namespace sym)) "Can't def ns-qualified name")
(let [name (:name (resolve-var (dissoc env :locals) sym))
init-expr (when (contains? args :init) (disallowing-recur
(analyze (assoc env :context :expr) (:init args) sym)))]
(merge {:env env :op :def :form form
:name name :doc (:doc args) :init init-expr}
(when init-expr {:children [init-expr]})))))
(defmethod parse 'fn*
[op env [_ & args] name]
(let [name (if (symbol? (first args))
(first args)
name)
meths (if (symbol? (first args))
(next args)
args)
;;turn (fn [] ...) into (fn ([]...))
meths (if (vector? (first meths)) (list meths) meths)
;;todo, merge meths, switch on arguments.length
meth (first meths)
params (first meth)
;;todo, variadics
params (remove '#{&} params)
body (next meth)
locals (reduce (fn [m name] (assoc m name {:name name})) (:locals env) params)
recur-frame {:names (vec params) :flag (atom nil)}
block (binding [*recur-frame* recur-frame]
(analyze-block (assoc env :context :return :locals locals) body))]
(assert (= 1 (count meths)) "Arity overloading not yet supported")
(merge {:env env :op :fn :name name :meths meths :params params :recurs @(:flag recur-frame)} block)))
(defmethod parse 'do
[op env [_ & exprs] _]
(merge {:env env :op :do} (analyze-block env exprs)))
(defn analyze-let
[encl-env [_ bindings & exprs :as form] is-loop]
(assert (and (vector? bindings) (even? (count bindings))) "bindings must be vector of even number of elements")
(let [context (:context encl-env)
[bes env]
(disallowing-recur
(loop [bes []
env (assoc encl-env :context :expr)
bindings (seq (partition 2 bindings))]
(if-let [[name init] (first bindings)]
(do
(assert (not (or (namespace name) (.contains (str name) "."))) (str "Invalid local name: " name))
(let [init-expr (analyze env init)
be {:name (gensym (str name "__")) :init init-expr}]
(recur (conj bes be)
(assoc-in env [:locals name] be)
(next bindings))))
[bes env])))
recur-frame (when is-loop {:names (vec (map :name bes)) :flag (atom nil)})
{:keys [statements ret children]}
(binding [*recur-frame* (or recur-frame *recur-frame*)]
(analyze-block (assoc env :context (if (= :expr context) :return context)) exprs))]
{:env encl-env :op :let :loop is-loop
:bindings bes :statements statements :ret ret :form form :children (into [children] (map :init bes))}))
(defmethod parse 'let*
[op encl-env form _]
(analyze-let encl-env form false))
(defmethod parse 'loop
[op encl-env form _]
(analyze-let encl-env form true))
(defmethod parse 'recur
[op env [_ & exprs] _]
(let [context (:context env)]
(assert *recur-frame* "Can't recur here")
(assert (= (count exprs) (count (:names *recur-frame*))) "recur argument count mismatch")
(reset! (:flag *recur-frame*) true)
(assoc {:env env :op :recur}
:frame *recur-frame*
:exprs (disallowing-recur (vec (map #(analyze (assoc env :context :expr) %) exprs))))))
(defmethod parse 'new
[_ env [_ ctor & args] _]
(disallowing-recur
(let [enve (assoc env :context :expr)
ctorexpr (analyze enve ctor)
argexprs (vec (map #(analyze enve %) args))]
{:env env :op :new :ctor ctorexpr :args argexprs :children (conj argexprs ctorexpr)})))
(defmethod parse 'set!
[_ env [_ target val] _]
(assert (symbol? target) "set! target must be a symbol naming var")
(assert (nil? (-> env :locals target)) "Can't set! local var")
(disallowing-recur
(let [enve (assoc env :context :expr)
targetexpr (analyze-symbol enve target)
valexpr (analyze enve val)]
{:env env :op :set! :target targetexpr :val valexpr :children [targetexpr valexpr]})))
(defmethod parse 'ns
[_ env [_ name & {:keys [requires macros] :as params}] _]
(doseq [nsym (vals macros)]
(require nsym))
(let [deps (into requires (map (fn [[alias nsym]]
[alias (find-ns nsym)])
macros))]
(swap! namespaces #(-> % (assoc-in [name :name] name)
(assoc-in [name :deps] deps))))
(merge {:env env :op :ns :name name} params))
(defn parse-invoke
[env [f & args]]
(disallowing-recur
(let [enve (assoc env :context :expr)
fexpr (analyze enve f)
argexprs (vec (map #(analyze enve %) args))]
{:env env :op :invoke :f fexpr :args argexprs :children (conj argexprs fexpr)})))
(defn analyze-symbol
"Finds the var associated with sym"
[env sym]
(let [ret {:env env :form sym}
lb (-> env :locals sym)]
(if lb
(assoc ret :op :var :info lb)
(assoc ret :op :var :info (resolve-var env sym)))))
(defn get-expander [sym env]
(when-not (-> env :locals sym)
))
(defn analyze-seq
[env form name]
(let [op (first form)]
(assert (not (nil? op)) "Can't call nil")
(if (specials op)
(parse op env form name)
(if-let [mac (and (symbol? op) (get-expander op env))]
(analyze (apply mac (rest form)))
(parse-invoke env form)))))
(defn analyze
"Given an environment, a map containing {:locals (mapping of names to bindings), :context
(one of :statement, :expr, :return), :ns (a symbol naming the
compilation ns)}, and form, returns an expression object (a map
containing at least :form, :op and :env keys). If expr has any (immediately)
nested exprs, must have :children [exprs...] entry. This will
facilitate code walking without knowing the details of the op set."
([env form] (analyze env form nil))
([env form name]
(let [form (if (instance? clojure.lang.LazySeq form)
(or (seq form) ())
form)]
(cond
(symbol? form) (analyze-symbol env form)
(and (seq? form) (seq form)) (analyze-seq env form name)
:else {:op :constant :env env :form form}))))
(comment
(in-ns 'clojure.cljs-01)
(import '[javax.script ScriptEngineManager])
(def manager (ScriptEngineManager.))
;; Nashorn 을 사용하기 위해서 Java11 로 변형후 실행이 필요.
(def jse (.getEngineByName manager "JavaScript"))
(.eval jse bootjs)
(def envx {:ns 'test.ns :context :return :locals '{ethel {:name ethel__123 :init nil}}})
(analyze envx nil)
(analyze envx 42)
(analyze envx "foo")
(analyze envx 'fred)
(analyze envx 'fred.x)
(analyze envx 'ethel)
(analyze envx 'ethel.x)
(analyze envx 'my.ns/fred)
(analyze envx 'your.ns.fred)
(analyze envx '(if test then else))
(analyze envx '(if test then))
(analyze (assoc envx :context :statement) '(def test "fortytwo" 42))
(analyze (assoc envx :context :expr) '(fn* [x y] x y x))
(analyze (assoc envx :context :statement) '(let* [a 1 b 2] a))
(analyze envx '(ns fred :requires {yn your.ns} :macros {core clojure.core}))
(defmacro js [form]
`(emit (analyze {:ns (symbol "test.ns") :context :expr :locals {}} '~form)))
(defn jseval [form]
(let [js (emits (analyze {:ns 'cljs.user :context :expr :locals {}}
form))]
(.eval jse (str "print(" js ")"))))
(js (def foo (fn* [x y] (if true 46 (recur 1 x)))))
(jseval '(ns fred :requires {yn your.ns} :macros {core clojure.core}))
(js (def x 42))
(jseval '(def x 42))
(jseval 'x)
(jseval '(if 42 1 2))
(jseval '(fn* [x y] (if true 46 (recur 1 x))))
(.eval jse "print(test.)")
(.eval jse "undefined !== false")
(js (def fred 42))
(js (new foo.Bar 65))
(doseq [e '[nil true false 42 "fred" fred ethel my.ns/fred your.ns.fred
(if test then "fooelse")
(def x 45)
(do x y y)
(fn* [x y] x y x)
(fn* [x y] (if true 46 (recur 1 x)))
(let* [a 1 b 2 a a] a b)
(do "do1")
(loop [x 1 y 2] (if true 42 (do (recur 43 44))))
(my.foo 1 2 3)
(let* [a 1 b 2 c 3] (set! y.s.d b) (new fred.Ethel a b c))
]]
(->> e
(analyze envx)
emit)
(newline))
)