From 934d82b2b6c78da1d1e6c6de7c9424ccdd99fed2 Mon Sep 17 00:00:00 2001
From: Alan Malloy <alan@malloys.org>
Date: Fri, 1 Jun 2012 11:59:54 -0700
Subject: [PATCH] Preserve &form metadata on macroexpanded forms

---
 src/jvm/clojure/lang/Compiler.java   |   30 +++++++++++++++++++++++++++++-
 test/clojure/test_clojure/macros.clj |   24 +++++++++++++++++++++++-
 2 files changed, 52 insertions(+), 2 deletions(-)

diff --git a/src/jvm/clojure/lang/Compiler.java b/src/jvm/clojure/lang/Compiler.java
index 0898f07..d4d96d1 100644
--- a/src/jvm/clojure/lang/Compiler.java
+++ b/src/jvm/clojure/lang/Compiler.java
@@ -80,6 +80,7 @@ static final Keyword inlineKey = Keyword.intern(null, "inline");
 static final Keyword inlineAritiesKey = Keyword.intern(null, "inline-arities");
 static final Keyword staticKey = Keyword.intern(null, "static");
 static final Keyword arglistsKey = Keyword.intern(null, "arglists");
+static final Keyword explicitMetaKey = Keyword.intern(null, "explicit-meta");
 static final Symbol INVOKE_STATIC = Symbol.intern("invokeStatic");
 
 static final Keyword volatileKey = Keyword.intern(null, "volatile");
@@ -6361,9 +6362,36 @@ public static Object macroexpand1(Object x) {
 		Var v = isMacro(op);
 		if(v != null)
 			{
+				IPersistentMap preMeta = null;
+				if (form instanceof IMeta)
+					{
+						preMeta = ((IMeta)form).meta();
+					}
 				try
 					{
-						return v.applyTo(RT.cons(form,RT.cons(LOCAL_ENV.get(),form.next())));
+						Object expansion = v.applyTo(RT.cons(form,RT.cons(LOCAL_ENV.get(),form.next())));
+						if (preMeta == null || !(expansion instanceof IObj))
+							{
+								return expansion;
+							}
+						IObj ret = (IObj)expansion;
+						IPersistentMap mergedMeta = ret.meta();
+						if (mergedMeta == null)
+							{
+								mergedMeta = PersistentArrayMap.EMPTY;
+							}
+
+                                                if (RT.booleanCast(RT.get(mergedMeta, explicitMetaKey)))
+                                                        {	// macro author has already considered &form meta
+                                                                return ret;
+                                                        }
+						ISeq pairs = preMeta.without(RT.LINE_KEY).without(RT.FILE_KEY).seq();
+						while (pairs != null)
+							{
+								mergedMeta = (IPersistentMap)mergedMeta.cons(pairs.first());
+								pairs = pairs.next();
+							}
+						return ret.withMeta(mergedMeta);
 					}
 				catch(ArityException e)
 					{
diff --git a/test/clojure/test_clojure/macros.clj b/test/clojure/test_clojure/macros.clj
index 711130e..0385862 100644
--- a/test/clojure/test_clojure/macros.clj
+++ b/test/clojure/test_clojure/macros.clj
@@ -9,10 +9,32 @@
 ; Author: Frantisek Sodomka
 
 (ns clojure.test-clojure.macros
-  (:use clojure.test))
+  (:use clojure.test)
+  (:import (java.util Map ArrayList)))
 
 ; http://clojure.org/macros
 
 ; ->
 ; defmacro definline macroexpand-1 macroexpand
 
+(defmacro simple-macro [f arg]
+  `(~f ~arg))
+
+(defmacro domineering-macro [arg]
+  (with-meta arg {:tag 'Integer, :explicit-meta true}))
+
+(let [expanded (macroexpand ' ^Integer (simple-macro inc 1))]
+  ;; calling macroexpand inside of deftest is broken, so do it outside
+  (deftest preserve-meta
+    (testing "These macros get applied"
+      (is (= 2 (simple-macro inc 1))))
+    (testing "Manual expansion preserves metadata"
+      (is (= 'Integer (:tag (meta expanded)))))
+    (testing "Compiler preserves metadata"
+      (is (thrown? ClassCastException (.size ^Map (simple-macro identity (ArrayList.))))))))
+
+(let [expanded (macroexpand ' ^String (domineering-macro (inc 1)))]
+  (deftest explicit-meta
+    (is (= '(inc 1) expanded))
+    (is (= 2 (domineering-macro (inc 1))))
+    (is (= 'Integer (:tag (meta expanded))))))
-- 
1.7.4.1

