[241222] tools.analyzer.jvm

Table of Contents

1 동기

clojure 의 core.async 는 원래 코드를 AST로 변환하는 로직을 자체적으로 구현해서 만들었지만 어느순간부터 tools.analyzer 라이브러리를 함께 사용하는 것 같다.

https://github.com/clojure/tools.analyzer

2 구성요소

2.1 AST (Abstract Syntax Tree)

Clojure 코드의 각 요소를 트리 구조로 표현한다.

각 노드에 아래와 같은 세부정보가 표현된다:

2.2 Passes (패스)

패스는 AST를 변환하거나 추가 분석을 수행하는 처리 단계. 예를 들어, 변수 바인딩 확인, 상수 전파, 타입 추론 등을 처리하는데 사용함. 개발자는 사용자 정의 패스를 작성하여 코드의 특정 패턴을 탐지하거나, 최적화를 적용할 수 있다.

2.3 코드로 말하기

의존성을 준비하자.

{:paths ["src"]
 :deps {org.clojure/tools.analyzer {:mvn/version "1.2.0"}}}

아래처럼 간단하게 사용할 수 있다.

(ns jvm-analyzer-demo
  (:require [clojure.tools.analyzer :as ana]
	    [clojure.tools.analyzer.env :as env]
	    [clojure.tools.analyzer.ast :as ast]
	    [clojure.tools.analyzer.jvm :as ana.jvm]
	    [clojure.tools.analyzer.passes :refer [schedule]]
	    [clojure.tools.analyzer.env :as env]
	    [clojure.tools.analyzer.passes.jvm.emit-form :as e]
	    [clojure.pprint :as pp]))

;; analyze 로 간단하게 AST 로 변환할 수  있다.
(ana.jvm/analyze 1)
;;=>
{:val 1,
 :type :number,
 :op :const,
 :env
 {:context :ctx/expr,
  :locals {},
  :ns jvm-analyzer-demo,
  :file "/path/src/jvm_analyzer_demo.clj"},
 :o-tag long,
 :literal? true,
 :top-level true,
 :form 1,
 :tag long}

;; AST에서 clojure form 으로 변환하려면 emit-form 을 쓴다.
(e/emit-form (ana.jvm/analyze '(let [a 1] a)))
;;=> (let* [a 1] a)

;; emit-form output 은 fully-macroexpanded 하다. 그러므로 capturing 문제가 있으므로
;; emit-hygienic-form 으로  hygienic form 을 출력할 수 있다.
(e/emit-hygienic-form (ana.jvm/analyze '(let [a 1 a a] a)))
;;=> (let* [a__#0 1 a__#1 a__#0] a__#1)

여기까지가 간단한 함수호출이다.

analyze 에는 environment 라는 개념이 있다. env를 추가하여 외부스코프에서 로컬 변수를 주입하는 것 같은 효과를 만들 수 있다.

(-> '(let [a a] a)
    (ana.jvm/analyze (assoc (ana.jvm/empty-env)
			    :locals '{a {:op :binding
					 :name a
					 :form a
					 :locals :let}}))
    e/emit-hygienic-form)
;; => (let* [a__#0 a] a__#0)

a 변수가 존재하는 것처럼 만들 수 있는 것이다.

analyze+eval 분석 후 양식을 평가하여 결과값을 ast :result 필드에 저장하는 함수 이 함수는 여러 form을 분석할 때 사용해야한다. clojure form 을 분석 할 때는, 이전 form을 평가해야 의미있을 수있다. 예시코드를 보자:

(ana.jvm/analyze+eval '(defmacro  x  []))
(ana.jvm/analyze+eval '(x))

첫번째값을 평가해고 다음 값을 평가해야 x를 이해할 수 있게 된다.

3 Custom Pass

이 부분이 가장 중요하다. 그리고 core.async 에서도 사용하는 설정이다.

우리에게 ~speical 라는 특별한 함수가 있다. 이 함수는 나중에 boom! 이라는 함수로 변환이 필요하다. 우리는 코드를 분석 할 때 special 이라는 함수가 있으면 이녀석은 변환이 필요하다고 마크해놓고, 어떤 함수로 변환이 필요한지 적어놓는다.

(defn var-name [v]
  (let [nm (:name (meta v))
	nsp (.getName ^clojure.lang.Namespace (:ns (meta v)))]
    (symbol (name nsp) (name nm))))

(defn transition-pass
  ;; pass 는 metadata 로 어떻게, 언제 수행할지를 설정할 수 있다. 이는 schedule 에 구현되어있음.
  {:pass-info {:walk :post :depends #{} :after an-jvm/default-passes}}
  [ast]
  (let [transitions (-> (env/deref-env) :passes-opts :transitions)]
    (if (and (= (:op ast) :invoke)       ;; AST 노드가 함수 호출
	     (= (-> ast :fn :op) :var)     ;; 변수인가
	     (contains? transitions (var-name (-> ast :fn :var))))  ;; 변환해야 하는 녀석인가.
      (do
	(merge ast
	       {:op :transitions
		:name (get transitions (var-name (-> ast :fn :var)))}))
      ast))) ;; 다른 노드는 변경하지 않음

;; 메타데이터는 아래코드로 알 수 있음.
(meta #'transition-pass)
(meta (var transition-pass))


;; 사용자 정의 패스를 추가한 패스 목록 생성
;; schedule 은 meta 함수의 메타데이터를 이용해서 설정값을 가진다.
;; {:after    #{...}  ; 이 패스 이전에 실행되어야 하는 패스들
;;  :before   #{...}  ; 이 패스 이후에 실행되어야 하는 패스들
;;  :depends  #{...}  ; 이 패스가 의존하는 패스들
;;  :walk     :none/:post/:pre/:any  ; 트리 순회 방식
;;  :affects  #{...}  ; 이 패스와 같은 트리 순회에 포함되어야 하는 패스들
;;  :state    fn      ; 패스 상태를 초기화하는 함수}
(def custom-passes
  (schedule (into an-jvm/default-passes #{#'transition-pass})))



;; 사용자 정의 패스를 추가한 패스 목록 생성
;; schedule 은 meta 함수의 메타데이터를 이용해서 설정값을 가진다.
;; {:after    #{...}  ; 이 패스 이전에 실행되어야 하는 패스들
;;  :before   #{...}  ; 이 패스 이후에 실행되어야 하는 패스들
;;  :depends  #{...}  ; 이 패스가 의존하는 패스들
;;  :walk     :none/:post/:pre/:any  ; 트리 순회 방식
;;  :affects  #{...}  ; 이 패스와 같은 트리 순회에 포함되어야 하는 패스들
;;  :state    fn      ; 패스 상태를 초기화하는 함수}
(def custom-passes
  (schedule (into an-jvm/default-passes #{#'transition-pass})))

;; 분석할 코드
(defn special [] "fOO")
(defn boom! [] "FFOO")
(def code '(special))

;; plus-to-multiply-pass  도입
(binding [ana.jvm/run-passes custom-passes]
  (-> (ana.jvm/analyze  code
			(ana.jvm/empty-env)
			{:passes-opts (merge ana.jvm/default-passes-opts {:transitions {`special `boom!}})})))
;;=>
{:args [],
 :children [:fn :args],
 :fn
 {:op :var,
  :assignable? false,
  :var #'jvm-analyzer-demo/special,
  :meta
  {:arglists ([]),
   :line 92,
   :column 1,
   :file "/src/jvm_analyzer_demo.clj",
   :name special,
   :ns #namespace[jvm-analyzer-demo]},
  :env
  {:context :ctx/expr,
   :locals {},
   :ns jvm-analyzer-demo,
   :column 12,
   :line 93,
   :file "/src/jvm_analyzer_demo.clj"},
  :form special,
  :o-tag java.lang.Object,
  :arglists ([])},
 :meta {:line 93, :column 12},
 :name jvm-analyzer-demo/boom!,
 :op :transitions,
 :env
 {:context :ctx/expr,
  :locals {},
  :ns jvm-analyzer-demo,
  :column 12,
  :line 93,
  :file "/src/jvm_analyzer_demo.clj"},
 :o-tag java.lang.Object,
 :top-level true,
 :form (special)}

위 출력값을 보면 :op 에 :transition 과 :name 에 jvm-analyzer-demo/boom! 이 저장되어 있는 것을 알 수 있다.

이는 core.async 에서 사용하는 mark-transition 함수의 의도와 동일하다. 이렇게 마킹을 해놓고 나중에 다시 코드를 실행할 때, :name 에 있는 함수를 사용하는 것이다.

Author: Younghwan Nam

Created: 2024-12-21 Sat 16:39

Emacs 27.2 (Org mode 9.4.4)

Validate