; Copyright (c) Chris Houser, Sep 2008-Jan 2009. 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. ; Reads Clojure code and emits equivalent JavaScript (ns clojure.contrib.clojurescript (:import (clojure.lang Compiler Compiler$C Compiler$BodyExpr Compiler$DefExpr Compiler$InstanceMethodExpr)) (:require [clojure.contrib.duck-streams :as ds])) (defn- vstr [v] (let [sb (StringBuilder.) lvl (fn lvl [v] (doseq [i v] (if (vector? i) (lvl i) (.append sb (str i)))))] (lvl v) (str sb))) (def *debug-fn-names* true) (def *debug-comments* true) (def *eval-defmacro* true) ; used internally (def *has-recur*) (def *local-names* {}) (defmulti #^{:private true} tojs (fn [e ctx] (class e))) (defn- fnmethod [fm maxm ctx] (let [lm (into {} (for [[lb lb] (.locals fm)] [lb (str (.name lb) "_" (.idx lb))])) thisfn (first (filter #(= 0 (.idx %)) (keys lm))) [body has-recur] (binding [*has-recur* false] [(tojs (.body fm) (merge-with merge ctx {:localmap lm})) *has-recur*]) mparm (into {} (for [p (.reqParms maxm)] [(.idx p) p])) inits (concat (when has-recur ["_cnt" "_rtn"]) (vals (reduce dissoc lm (conj (.reqParms fm) thisfn (.restParm fm)))) (when (:fnname ctx) [(str (lm thisfn) "=arguments.callee")]) (when (not= fm maxm) (for [lb (.reqParms fm) :when (not= (.name lb) (.name (mparm (.idx lb))))] [(lm lb) "=arguments[" (dec (.idx lb)) "]"])) (when-let [lb (.restParm fm)] [(str (lm lb) "=clojure.JS.rest_args(this,arguments," (count (.reqParms fm)) ")")]))] (.reqParms maxm) (vstr [(when (seq inits) [(apply vector "var " (interpose "," inits)) ";\n"]) (if has-recur ["do{_cnt=0;_rtn=" body "\n}while(_cnt);return _rtn;"] ["return (" body ")"])]))) (defmethod tojs clojure.lang.Compiler$FnExpr [e ctx] (let [maxm (or (.variadicMethod e) (-> (into (sorted-map) (for [fm (.methods e) :when (not= fm (.variadicMethod e))] [(count (.reqParms fm)) fm])) last val)) manym (< 1 (count (.methods e))) newctx (assoc ctx :fnname (.thisName e)) [methods local-names] (binding [*local-names* *local-names*] [(into {} (for [fm (.methods e)] [fm (fnmethod fm maxm newctx)])) *local-names*])] (vstr [(when (.variadicMethod e) ["clojure.JS.variadic(" (count (.reqParms maxm)) ","]) "(function" (when *debug-fn-names* [" __" (.replaceAll (.name e) "[\\W_]+" "_")]) "(" (vec (interpose "," (for [lb (.reqParms maxm)] [(.name lb) "_" (.idx lb)]))) "){" ;"\n//" (vec (interpose "," (vals local-names))) "\n" (when manym ["switch(arguments.length){" (vec (for [[fm body] methods :when (not= fm maxm)] ["\ncase " (count (.reqParms fm)) ":" body])) "}"]) "\n" (methods maxm) "})" (when (.variadicMethod e) ")") ]))) (defmethod tojs clojure.lang.Compiler$BodyExpr [e ctx] (apply str (interpose ",\n" (map #(tojs % ctx) (.exprs e))))) (defmethod tojs clojure.lang.Compiler$LetExpr [e ctx] (let [inits (vec (interpose ",\n" (for [bi (.bindingInits e)] ["(" ((:localmap ctx) (.binding bi)) "=" (tojs (.init bi) ctx) ")"])))] (if (.isLoop e) (binding [*has-recur* false] (vstr ["((function" (when *debug-fn-names* " __loop") "(){var _rtn,_cnt;" inits ";" "do{_cnt=0;\n_rtn=" (tojs (.body e) ctx) "}while(_cnt);return _rtn;})())"])) (vstr ["(" inits ",\n" (tojs (.body e) ctx) ")"])))) (defmethod tojs clojure.lang.Compiler$VectorExpr [e ctx] (vstr ["clojure.JS.lit_vector([" (vec (interpose "," (map #(tojs % ctx) (.args e)))) "])"])) (defn- const-str [c] (cond (or (instance? Character c) (string? c)) (pr-str (str c)) (keyword? c) (str "clojure.core.keyword(\"" (namespace c) "\",\"" (name c) "\")") (symbol? c) (str "clojure.core.symbol(\"" c "\")") (class? c) (.getCanonicalName c) (list? c) (vstr ["clojure.JS.lit_list([" (vec (interpose "," (map const-str c))) "])"]) (fn? c) (str \" c \") (instance? java.util.regex.Pattern c) (str "(/" (.replace (str c) "/" "\\/") "/)") :else (str "(" c ")"))) (defmethod tojs clojure.lang.Compiler$ConstantExpr [e ctx] (const-str (.v e))) (def js-reserved '#{import boolean short byte char class}) (defn- var-munge [x] (let [n (-> x str Compiler/munge (.replace "." "_DOT_"))] (if (js-reserved (symbol n)) (str n "_") n))) (defn- var-parts [e] (let [{:keys [name ns]} (meta (.var e))] [(Compiler/munge (str (.getName ns))) (var-munge name)])) (defmethod tojs clojure.lang.Compiler$UnresolvedVarExpr [e ctx] (vstr ["clojure.JS.resolveVar(\"" (var-munge (name (.symbol e))) "\"," (Compiler/munge (name (.name *ns*))) ")"])) (defmethod tojs clojure.lang.Compiler$VarExpr [e ctx] (let [[vns vname] (var-parts e)] (if (and (= vns "clojurescript.js") (#{"this"} vname)) vname (str vns "." vname)))) (defmethod tojs clojure.lang.Compiler$TheVarExpr [e ctx] (let [[vns vname] (var-parts e)] (str vns "._var_" vname))) (defmethod tojs clojure.lang.Compiler$AssignExpr [e ctx] (let [target (.target e)] (if (instance? clojure.lang.Compiler$InstanceFieldExpr target) (vstr ["(" (tojs (.target target) ctx) "." (var-munge (.fieldName target)) "=" (tojs (.val e) ctx) ")"]) (let [[vns vname] (var-parts target)] (str vns "._var_" vname ".set(" (tojs (.val e) ctx) ")"))))) (defmethod tojs clojure.lang.Compiler$DefExpr [e ctx] (let [[vns vname] (var-parts e)] (str "clojure.JS.def(" vns ",\"" vname "\"," (tojs (.init e) ctx) ")"))) (defmethod tojs clojure.lang.Compiler$InvokeExpr [e ctx] (vstr [(tojs (.fexpr e) ctx) ".apply(null,[" (vec (interpose "," (map #(tojs % ctx) (.args e)))) "])"])) (defmethod tojs clojure.lang.Compiler$LocalBindingExpr [e ctx] (let [local-name ((:localmap ctx) (.b e))] (set! *local-names* (assoc *local-names* (.b e) local-name)) local-name)) (defmethod tojs clojure.lang.Compiler$NilExpr [e ctx] "null") (defmethod tojs clojure.lang.Compiler$EmptyExpr [e ctx] (str (.getCanonicalName (class (.coll e))) ".EMPTY")) (defmethod tojs clojure.lang.Compiler$StringExpr [e ctx] (const-str (.str e))) (defmethod tojs clojure.lang.Compiler$KeywordExpr [e ctx] (const-str (.k e))) (defmethod tojs clojure.lang.Compiler$StaticFieldExpr [e ctx] (str "clojure.JS.getOrRun(" (.getCanonicalName (.c e)) ",\"" (var-munge (.fieldName e)) "\")")) (defmethod tojs clojure.lang.Compiler$StaticMethodExpr [e ctx] (vstr [(.getCanonicalName (.c e)) "." (.methodName e) "(" (vec (interpose "," (map #(tojs % ctx) (.args e)))) ")"])) (defmethod tojs clojure.lang.Compiler$NewExpr [e ctx] (vstr ["(new " (.getCanonicalName (.c e)) "(" (vec (interpose "," (map #(tojs % ctx) (.args e)))) "))"])) (defmethod tojs clojure.lang.Compiler$InstanceMethodExpr [e ctx] (vstr ["(" (tojs (.target e) ctx) ")." (var-munge (.methodName e)) "(" (vec (interpose "," (map #(tojs % ctx) (.args e)))) ")"])) (defmethod tojs clojure.lang.Compiler$InstanceFieldExpr [e ctx] (vstr ["clojure.JS.getOrRun(" (tojs (.target e) ctx) ",\"" (var-munge (.fieldName e)) "\")"])) (defmethod tojs clojure.lang.Compiler$IfExpr [e ctx] (str "((" (tojs (.testExpr e) ctx) ")?(" (tojs (.thenExpr e) ctx) "):(" (tojs (.elseExpr e) ctx) "))")) (defmethod tojs clojure.lang.Compiler$RecurExpr [e ctx] (set! *has-recur* true) (vstr ["(_cnt=1,_rtn=[" (vec (interpose "," (map #(tojs % ctx) (.args e)))) "]" (vec (map #(str "," ((:localmap ctx) %1) "=_rtn[" %2 "]") (.loopLocals e) (iterate inc 0))) ")"])) (defmethod tojs clojure.lang.Compiler$MapExpr [e ctx] (vstr ["clojure.core.hash_map(" (vec (interpose "," (map #(tojs % ctx) (.keyvals e)))) ")"])) (defmethod tojs clojure.lang.Compiler$SetExpr [e ctx] (vstr ["clojure.core.hash_set(" (vec (interpose "," (map #(tojs % ctx) (.keys e)))) ")"])) (defmethod tojs clojure.lang.Compiler$BooleanExpr [e ctx] (if (.val e) "true" "false")) (defmethod tojs clojure.lang.Compiler$ThrowExpr [e ctx] (vstr ["(function" (when *debug-fn-names* " __throw") "(){throw " (tojs (.excExpr e) ctx) "})()"])) (defmethod tojs clojure.lang.Compiler$TryExpr [e ctx] (vstr ["(function" (when *debug-fn-names* " __try") "(){try{var _rtn=(" (tojs (.tryExpr e) ctx) ")}" (when (seq (.catchExprs e)) (when (not= 1 (count (.catchExprs e))) (throw (Exception. "tojs only supports one catch clause per try"))) (let [cc (first (.catchExprs e))] ["\ncatch(" ((:localmap ctx) (.lb cc)) "){_rtn=" (tojs (.handler cc) ctx) "}"])) (when (.finallyExpr e) ["\nfinally{" (tojs (.finallyExpr e) ctx) "}"]) "return _rtn})()"])) (defmulti toclj class) (defmethod toclj clojure.lang.Compiler$KeywordExpr [e] (.k e)) (defmethod toclj clojure.lang.Compiler$StringExpr [e] (.str e)) (defmethod toclj clojure.lang.Compiler$ConstantExpr [e] (.v e)) (def skip-def '#{;-- implemented directly in clj.js seq instance? assoc apply refer first rest import hash-map count find keys vals get class contains? print-method class? number? string? integer? nth to-array cons keyword symbol load ;-- not supported yet make-array to-array-2d re-pattern re-matcher re-groups re-seq re-matches re-find format ;-- macros defined without using defmacro let loop fn defn defmacro ;-- will probably never be supported in clojurescript eval resolve ns-resolve await await-for macroexpand macroexpand-1 load-reader load-string special-symbol? bigint bigdec floats doubles ints longs float-array double-array int-array long-array aset-int aset-long aset-boolean aset-float aset-double aset-short aset-char aset-byte slurp seque decimal? float? pmap primitives-classnames}) (def skip-method #{"java.lang.Class"}) (defn formtojs [f] (when-not (and (coll? f) (= 'definline (first f))) (let [expr (binding [*allow-unresolved-vars* true *compiler-analyze-only* true *private-compiler-loader* (clojure.lang.RT/baseLoader)] (Compiler/analyze Compiler$C/STATEMENT `((fn [] ~f)))) mainexpr (-> expr .fexpr .methods first .body .exprs first) defmacro? (and (instance? Compiler$BodyExpr mainexpr) (instance? Compiler$DefExpr (first (.exprs mainexpr))) (instance? Compiler$InstanceMethodExpr (second (.exprs mainexpr))) (= "setMacro" (.methodName (second (.exprs mainexpr)))))] (if defmacro? (when *eval-defmacro* (eval f) nil) (when-not (or (and (instance? Compiler$DefExpr mainexpr) (skip-def (:name (meta (.var mainexpr))))) (and (instance? Compiler$InstanceMethodExpr mainexpr) (or (= "setMacro" (.methodName mainexpr)) (and (= "addMethod" (.methodName mainexpr)) (skip-method (tojs (first (.args mainexpr)) nil))))) (and (instance? Compiler$BodyExpr mainexpr) (instance? Compiler$DefExpr (first (.exprs mainexpr))) (instance? Compiler$InstanceMethodExpr (second (.exprs mainexpr))) (= "setMacro" (.methodName (second (.exprs mainexpr)))))) (tojs expr {:localmap {}})))))) (defn filetojs [filename & optseq] (let [reader (java.io.PushbackReader. (ds/reader filename)) opts (apply array-map optseq)] (binding [*ns* (create-ns 'user) *debug-fn-names* (:debug-fn-names opts true) *debug-comments* (:debug-comments opts true) *eval-defmacro* (:eval-defmacro opts true)] (loop [] (let [f (read reader false reader false)] (when-not (identical? f reader) (if-let [js (formtojs f)] (do (when *debug-comments* (println "\n//======") (print "//") (prn f) (println "//---")) (println (str js ";")) (when (and (coll? f) (or (= 'ns (first f)) (= 'in-ns (first f)))) (eval f))) (when *debug-comments* (print "// Skipping: ") (prn f))) (recur)))))))