Detecting ClojureScript compilation in macros

1,066 views
Skip to first unread message

Максим Карандашов

unread,
Jun 3, 2014, 8:07:39 AM6/3/14
to clojur...@googlegroups.com
I want to find (in the macro call) what happens: Clojure or ClojureScript compilation. It needed for using one name of macro inside Clojure and ClojureScript.

Currently I use next approach:

(if (nil? cljs.env/*compiler*)
(do-something-for-clojure)
(do-something-for-clojurescript))

And it's working well. But maybe this is not correct? Or is there another way to do this?

Ambrose Bonnaire-Sergeant

unread,
Jun 3, 2014, 8:40:54 AM6/3/14
to clojur...@googlegroups.com
I would write two different macros.

Thanks,
Ambrose



--
Note that posts from new members are moderated - please be patient with your first post.
---
You received this message because you are subscribed to the Google Groups "ClojureScript" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojurescrip...@googlegroups.com.
To post to this group, send email to clojur...@googlegroups.com.
Visit this group at http://groups.google.com/group/clojurescript.

Nahuel Greco

unread,
Jun 3, 2014, 10:21:35 AM6/3/14
to clojur...@googlegroups.com
check what Prismatic schema uses: https://github.com/Prismatic/schema/blob/master/src/clj/schema/macros.clj#L13-20

Saludos,
Nahuel Greco.

Chas Emerick

unread,
Jun 3, 2014, 10:29:59 AM6/3/14
to clojur...@googlegroups.com
My preferred approach is to check `(:ns &env)`; it will be nil in Clojure. This helpfully means that the macro in question has no dependency on the ClojureScript compiler, and so can be used without the CLJS dependency if desired.

If at all possible, please don't write two different macros. Libraries that do this are just making more work for those that want to maintain portable codebases (cljx is a workaround here), and the typical namespacing of the different implementations are usually easy to transpose when reading or typing, leading to unpleasant debugging cycles. (e.g. `foo.clj/some-macro` vs `foo.cljs/some-macro`)

- Chas

Yehonathan Sharvit

unread,
Jun 10, 2014, 12:18:44 AM6/10/14
to clojur...@googlegroups.com
Why clix is a workaround and not considered as the best solution?

Jason Wolfe

unread,
Jun 17, 2014, 10:01:46 PM6/17/14
to clojur...@googlegroups.com
Not anymore (thanks Chas):

https://github.com/Prismatic/schema/pull/113

Nahuel Greco

unread,
Jun 19, 2014, 1:31:32 AM6/19/14
to clojur...@googlegroups.com
I found a problem in the Chas approach. When you call a macro from a cljs file, and that macro calls another macro with a(:ns &env) test, it sees a Clojure &env, so it thinks is being compiled in a CLJ environment. This doesn't happens with the Prismatic approach (the old one, it was recently changed for the Chas one). See below:

;;;----- p/macros.clj ------
(ns p.macros)

(defn log [s]
  (spit "/tmp/log" (str (pr-str s) "\n") :append true))

(defmacro if-cljs      ;; Chas approach 
  [then else]
  (log [:if-cljs-env &env])
  (if (:ns &env) then else))


(defn compiling-cljs?  ;; Prismatic (old) approach
  []
  (boolean
   (when-let [n (find-ns 'cljs.analyzer)]
     (when-let [v (ns-resolve n '*cljs-file*)]
       @v))))

(defmacro other-macro
  []
  (log [:other-macro-env &env]) 
  (log [:if-cljs (if-cljs :cljs :clj)])
  (log [:if-compiling-cljs (if (compiling-cljs?) :cljs :clj)]))

;;;----- p/core.cljs ------
(ns p.macros
   (:require-macros [p.macros]))

(p.macros/other-macro)

When you run this, you get the following contents in the /tmp/log file:

[:if-cljs-env {&env #<LocalBinding clojure.lang.Compiler$LocalBinding@18b3f8f5>, &form #<LocalBinding clojure.lang.Compiler$LocalBinding@7a69c3fb>}]
[:other-macro-env {:column 1, :line 14, :ns {:defs  .... }}]
[:if-cljs :clj]                  ;;; WRONG!
[:if-compiling-cljs :cljs]       ;;; CORRECT

Note, [:if-cljs-env ...] is printed before [:if-cljs ...] as the if-cljs macro expands inside the other-macro definition before other-macro is executed from cljs. 

My question is, why the &env in the if-cljs macro call here is not a ClojureScript one? Bug or feature? :) 


Saludos,
Nahuel Greco.

Jason Wolfe

unread,
Jun 19, 2014, 2:00:11 AM6/19/14
to clojur...@googlegroups.com
The problem with our old solution is that if you called a macro in cljx code, and that fn was itself called from Clojure code involved in macroexpansion of ClojureScript, it would get the cljs version and barf.  

I think our current solution mostly solves this, in that macros that want to emit cross-platform code should emit forms like `(if-cljs cljs-form clj-form) -- notice the backtick -- and even if they are called from other macros, the ultimate expansion takes place in the correct target context (I hope).  

If a macro wants to directly switch on the env (rather than emitting an if-cljs form) the env must be passed in from the parent, but in general I think it seems like a better approach.  In any case, it seems to have solved our immediate issues that some people were having with cljs compilation, without breaking any tests.  

In your specific example, there's no backtick before "if-cljs", which I believe is causing the unexpected behavior (although I think your log fn would have to be changed as well to emit a form, as is typical for macro helper fns).  But I haven't actually tested this, so if this is incorrect please let us know.  


You received this message because you are subscribed to a topic in the Google Groups "ClojureScript" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/clojurescript/iBY5HaQda4A/unsubscribe.
To unsubscribe from this group and all its topics, send an email to clojurescrip...@googlegroups.com.

Nahuel Greco

unread,
Jun 19, 2014, 2:29:43 AM6/19/14
to clojur...@googlegroups.com
Jason, thanks for the quick reply. I understand your point, but sometimes is better to have an in-situ check without needing to return an if-cljs form, like in:

(defmacro aaa []
    (let [xxx  (if-cljs yyy zzz)]
        ... do something complex with xxx))

(defmacro bbb []
   (aaa))


So as you said the alternative options are:

A- refactorize aaa to return `(if-cljs ...) forms... always possible?
B- add an explicit env parameter to aaa, but then all your macro libraries intended to be used from CLJ&CLJS must follow this style.
C- use the old approach, but is incompatible with having 'cljs.analyzer loaded for other reasons. 

I feel there is an unsatisfied need here, but maybe A is sufficient. Also, I would like to know if there is a rationale behind &env being in a way (CLJS) in the first macro level and in another way (CLJ) in the next ones, if is intended or is an implementation limitation.


Saludos,
Nahuel Greco.

Jason Wolfe

unread,
Jun 19, 2014, 1:02:01 PM6/19/14
to clojur...@googlegroups.com
On Wednesday, June 18, 2014 11:29:43 PM UTC-7, Nahuel Greco wrote:
> Jason, thanks for the quick reply. I understand your point, but sometimes is better to have an in-situ check without needing to return an if-cljs form, like in:
>
>
>
>
> (defmacro aaa []
>     (let [xxx  (if-cljs yyy zzz)]
>         ... do something complex with xxx))
>
> (defmacro bbb []
>
>
>    (aaa))
>
>
> So as you said the alternative options are:
>
> A- refactorize aaa to return `(if-cljs ...) forms... always possible?
>
>
> B- add an explicit env parameter to aaa, but then all your macro libraries intended to be used from CLJ&CLJS must follow this style.
>
>
> C- use the old approach, but is incompatible with having 'cljs.analyzer loaded for other reasons. 
>
> I feel there is an unsatisfied need here, but maybe A is sufficient. Also, I would like to know if there is a rationale behind &env being in a way (CLJS) in the first macro level and in another way (CLJ) in the next ones, if is intended or is an implementation limitation.

I'm in over my head a little bit here, but my guess is that preferring (A), and falling back to (B) if you can't/don't want to expand to if-cljs is probably not a huge burden. I think generally calling a custom macro from another macro (rather than expanding into it) is also pretty uncommon in my experience (excluding built-in clojure macros).

I also think (C) is just a natural consequence of what macros are and how they are called -- a macro is just a function, so when calling a macro from another macro, the environment is naturally that of the first Clojure macro, not the outermost function.
Reply all
Reply to author
Forward
0 new messages