[241220] deep walking macro
Table of Contents
1 동기
deep walking macro 는 Timothy Baldridge 는 core.async 의 go 매크로를 만드는데 중심역할을 한 사람인 것 같다. 나는 아래 링크의 유튜브 영상을 몇년 전에 보았고, 오랫만에 다시 보았는데 아주 멋진 강의였다고 생각한다. 그래서 영상의 내용을 글로 적어 놓는 것이 아주 좋을 것 같다고 생각했다. 나 또한 다른 블로그를 보면 매크로에 대해 많은 관심을 가진 적이 있기 때문에
https://www.youtube.com/watch?v=HXfDK1OYpco&ab_channel=TimothyBaldridge
2 매크로
clojure에서 매크로와의 차이는 딱하나 meta 데이터에 macro 여부 변수다.
(. (var defmacro) (setMacro))
2.1 Var
setMacro를 가진 곳은 Var.java 객체이다.  
Var 는 변수를 담는 박스같은 것이다. 변수를 정의하거나 값을 참조할 때 사용된다. 동적으로 재정의할 수 있으며, 상태(state)와 값을 분리하는데 유용한다.
(def x) #'user/x user=> x #object[clojure.lang.Var$Unbound 0x14008db3 "Unbound: #'user/x"] (def x 1) #'user/x user=> x 1
기본적으로 Var는 static 이지만, binding을 통해 동적으로 표시할 수 있음.
user=> (def ^:dynamic x 1) user=> (def ^:dynamic y 1) user=> (+ x y) 2 user=> (binding [x 2 y 3] (+ x y)) 5 user=> (+ x y) 2
Var.java 의 구현체에 setMacro 메소드가 존재한다.
static Keyword macroKey = Keyword.intern(null, "macro");
public void setMacro() {
    alterMeta(assoc, RT.list(macroKey, RT.T))
}
public boolean isMacro(){
	return RT.booleanCast(meta().valAt(macroKey));
}
여기서 RT.T  는 true 를 의미한다.
alterMeta 함수는 IReference 에 존재한다:
package clojure.lang;
public interface IReference extends IMeta {
    IPersistentMap alterMeta(IFn alter, ISeq args) ;
    IPersistentMap resetMeta(IPersistentMap m);
}
Var.java 는 AReference 를 확장하고 있다:
public abstract class ARef extends AReference implements IRef{ ... }
AReference 구현을 보자. IReference 인터페이스를 맵으로 구현한다:
package clojure.lang;
public class AReference implements IReference {
    private IPersistentMap _meta;
    public AReference() {
	this(null);
    }
    public AReference(IPersistentMap meta) {
	_meta = meta;
    }
    synchronized public IPersistentMap meta() {
	return _meta;
    }
    synchronized public IPersistentMap alterMeta(IFn alter, ISeq args)  {
	_meta = (IPersistentMap) alter.applyTo(new Cons(_meta, args));
	return _meta;
    }
    synchronized public IPersistentMap resetMeta(IPersistentMap m) {
	_meta = m;
	return m;
    }
}
단순히 setMacro 를 이용해서 macro 여부만 갖고 있게된 것이 매크로이지만 이것으로 컴파일 수행되기 이전에 코드가 확장된다. 이를 매크로확장이라고 한다.
(macroexpand '(when (when x y) z)) ;-> (if (if x (do y)) (do z)) ; 모든 when이 완전히 확장됩니다
macroexpand 는 컴파일 이전에 수행된다고 했다. 어디서 수행되는가? 답은 Compiler.java 에 있다:
class Compiler  {
  static final public IPersistentMap specials = PersistentHashMap.create(
    DEF, new DefExpr.Parser(),
    LOOP, new LetExpr.Parser(),
    RECUR, new RecurExpr.Parser(),
    IF, new  IfExpr.Parser(),
    CASE, new CaseExpr.Parser(),
    LET, new  LetExpr.Parser(),
    Do, new BodyExpr.Parser(),
    FN, nul
    // ...
  )
  static boolean isSpecial(object sym) {
    return specials.containsKey(sym);
  }
  static Object macroexpand(Object form) {
    Object exf = macroexpand1(form);
    if(exf != form)  // 매크로 확장이 수행되었다면, 다시 매크로확장을 해본다.
      return macroexpand(exf);
    return form
  }
  static Object macroexpand1(Object x)  {
    // x가 ISeq(시퀀스)가 아니면 매크로 확장이 불가능하므로 그대로 반환
    if(x instanceof ISeq) {
	ISeq form = (ISeq) x;
	Object op =  RT.first(form);
	if(isSpecial(op)) return x;
	Var v = isMacro(op); // if it's macro, it will be a non-null object.
	if (v != null) {
	    checkSpecs(v, form);
	    try {
		ISeq args = RT.cons(form, RT.cons(Compiler.LOCAL_ENV.get(), form.next()));
		return v.applyTo(args);  // 매크로 확장이 일어나는 부분.
	    } catch(...) {...}
	} else {
	    // 문법적 변환 부분 (매크로 확장과는 다름)
	    // Java 메서드 호출과 생성자 호출을 위한 구문 변환
	    // (.method obj args) -> (. obj method args)
	    // (ClassName. args) -> (new ClassName args)        }
    }
    return  x;
  }
}
v.applyTo(args) 로 평가를 하면 매크로는 코드를 만들어서 리턴하기 때문에 매크로확장이 된다.
2.2 Environment
매크로에서는 Environment 로 환경을 가져올 수 있음
(macroexpand '(defmacro foo [] 42))
=>
(do 
    (clojure.core/defn foo ([&form &env] 42))
    (. (var foo) (setMacro))
    (var foo)
)
그러므로 아래 매크로를 실행하면 environment를 출력할 수 있다.
(defmacro when [test & body]
    (println &env)
    `(if ~test
	(do ~body)
	nil))
(fn [x]
  (when true (println "hello" 42)))
=> {x #<LocalBinding clojure.lang.Compiler$LocalBinding@194527c6>}
이번엔
(meta #'when)
{:arglists ([test & body]), :ns #<Namespace deep-walking-macro.core>, :name when
 :column 1 :line 22
 :macro true  ;; 이게 중요
}
3 deep walking macro
영상에서 말하는 core.async 의 go 매크로에서 주요로직이 되는 코드를 AST(Abstract Syntax Tree) 로 변환하는 파서를 구현한다.
parse-item 은 기본타입분류. parse-sexpr 는 S-expression 분석이다.
간단한 구조로는 모든 AST 는 최소한 :type 키를 가진다.
또한 ctx 는 아직  쓸일이 없어서 atom을 이용한 int의 개수를 카운트하는 기능을 구현해봄으로써 어떻게 ctx가 사용될 수 있는지 알 수 있는 대목이다.
(defmulti parse-item (fn [form ctx]
		       (cond
			 (seq? form) :seq
			 (int? form) :int
			 (symbol? form) :symbol
			 (nil? form) :nil)))
(defmulti parse-sexpr (fn [[sym & rest] ctx]
			sym))
#_(defmethod parse-item :seq
    [form ctx]
    (parse-sexpr form ctx))
(defmethod parse-item :seq
  [form ctx]
  (let [form (macroexpand form)]
    (parse-sexpr form ctx)))
#_(defmethod parse-item :int
    [form ctx]
    {:type :int
     :value form})
(defmethod parse-item :int
  [form ctx]
  (swap! ctx inc)
  {:type :int
   :value form})
(defmethod parse-item :symbol
  [form ctx]
  {:type :symbol
   :value form})
(defmethod parse-item :nil
  [form ctx]
  {:type :nil})
(defmethod parse-sexpr 'if
  ;; 첫번째 인자는 필요없음 if 인걸 알고 있음.
  [[_ test then else] ctx]
  {:type :if
   :test (parse-item test ctx)
   :then (parse-item then ctx)
   :else (parse-item else ctx)})
   ;; 이를 보면 우리는 각 표현식을 recursive 하게 해시맵을 만들고 있고
   ;; 컴파일러는 배우고 있는 사람에게는 이건 AST 와 같음을 알 수 있다.
(defmethod parse-sexpr 'do
  [[_ & body]  ctx]
  {:type :do
   :body (doall (map (fn [x] (parse-item x ctx)) body))})
(comment
  (parse-item 42 nil)
  ;; {:type :int, :value 42}
  (parse-item 's nil)
  ;; {:type :symbol, :value s}
  (parse-item '(if x 42 41) nil)
  ;; {:type :if, :test {:type :symbol, :value x}, :then {:type :int, :value 42}, :else {:type :int, :value 41}}
  (parse-item '(when  x 42 1) nil)
  ; Execution error (IllegalArgumentException) at core/eval10119$fn (REPL:16).
  ; No method in multimethod 'parse-sexpr' for dispatch value: when
  (parse-item '(when  x 42 1) nil)
  ;; 두번째 개선 후.
  ; Execution error (IllegalArgumentException) at core/eval10424$fn (REPL:21).
  ; No method in multimethod 'parse-sexpr' for dispatch value: do
  (parse-item '(when  x 42) {})
  #_{:type :if,
     :test {:type :symbol, :value x},
     :then {:type :do, :body ({:type :int, :value 42} {:type :int, :value 1})},
     :else {:type :nil}}
  #_(defmacro when [test & body]
      `(if ~test
	 (do ~body)
	 nil))
  ;;
  (let [a (atom 0)]
    (parse-item '(when t 42) a)
    (println @a))
  (def b (atom 0))
  (parse-item '(when t 42) b)
  @b
  (defmethod  parse-sexpr :default
    [[f &  body] ctx]
    {:type  :call
     :fn (parse-item f ctx)
     :args (doall (map (fn [x] (parse-item x ctx))
		       body))})
  (parse-item '(+ 2 3) (atom 0))
  ;; {:type :call, :fn {:type :symbol, :value +}, :args ({:type :int, :value 2} {:type :int, :value 3})}
  (parse-item '((comp pos? +) 2 3) (atom 0))
  ;; {:type :call,
  ;;  :fn {:type :call, :fn {:type :symbol, :value comp}, :args ({:type :symbol, :value pos?} {:type :symbol, :value +})},
  ;;  :args ({:type :int, :value 2} {:type :int, :value 3})}
  (parse-item '(+ 2 3) (atom 0))
  ;; {:type :call, :fn {:type :symbol, :value +}, :args ({:type :int, :value 2} {:type :int, :value 3})}
  (parse-item '((comp pos? +) 2 3) (atom 0))
  ;; {:type :call,
  ;;  :fn {:type :call, :fn {:type :symbol, :value comp}, :args ({:type :symbol, :value pos?} {:type :symbol, :value +})},
  ;;  :args ({:type :int, :value 2} {:type :int, :value 3})}
  (defmacro to-ast [form]
    (parse-item form (atom 0)))
  (to-ast (+ 1 2))
  )