genuine-highlighter: マクロを認識するClojure向けのシンタックスハイライター

Preview:

DESCRIPTION

Shibuya.lisp テクニカルトーク #8 で話した内容です。

Citation preview

genuine-highlighter:!!

マクロを認識するClojure向けの!シンタックスハイライター

Shibuya.lisp TT #8!14/08/30 @athos0220

自己紹介

• 名古屋圏内在住

• 組込み業界で働くプログラマ

• 絶賛転職活動中@athos0220

シンタックスハイライト

シンタックスハイライト

Javaのシンタックスハイライト結果 on Emacs

シンタックスハイライト• コード中の重要な部分を強調し、重要でない部分を目立たなくすることでコードを読む効率を上げる

• 予約語などを強調

• コメントなどを薄めに

• 予約語のハイライトでつづり間違いに気づく作用も

• 多くのエディタやコードビューワで使われているごくありふれた機能

でも精度よくないことが多い

ハイライトの仕方に一貫性がない

精度よくないといけないのか?

• 重要なものを強調し、重要でないものを目立たなくすることでコードを読む効率を上げる

• 人は誤ったハイライトを繰り返し目にすることでハイライトを無視するようになる[要出典]

そんなに簡単な仕事ではない

“catch me if you can”; 文字列中のキーワードをハイライトしてしまう

(quote (if)); クオートの中のキーワードをハイライトしてしまう

(let [name #(subs (name %) 1)] (name ’foo)); トップレベル変数とローカル変数のハイライトが一貫; していない

シンタックスハイライターで起こりがちなミス

Lisp系言語に固有の難しさ

• 予約語がない

• 構文キーワードをローカル変数でシャドウイングできる

• ユーザがマクロを使って独自構文を定義できる

• シンボルがどう使われるかはマクロを展開してみないと分からない

genuine-highlighter

genuine-highlighterの狙い• シンボルの使われ方を(可能な限り)忠実にハイライトし分けられるハイライターを実現する • Var • ローカル変数 • 特殊形式 • マクロ

• 特定の出力形式(eg. HTML)に依存しないポータブルな実装

genuine-highlighterの特徴

• マクロを認識するClojure向けのシンタックスハイライター

• ユーザが新しい出力形式へ対応させられるように拡張できる

genuine-highlighterの特徴

• マクロを認識するClojure向けのシンタックスハイライター!

• ユーザが新しい出力形式へ対応させられるように拡張できる

parse

analyze

render

source code

object format

parse

analyze

render

source code

object format

• Clojureリーダで読み込むとそのまま書き戻せない

• コメントが削除される • インデントが崩れる • リーダマクロが展開される

• 読み込んだ後にそのまま書き戻せる中間形式へパース(Sjacket)

parse

analyze

render

source code

object format

’(foo bar)

{:tag :net.cgrand.sjacket.parser/root, :content [{:tag :quote, :content ["'" {:tag :list, :content ["(" {:tag :symbol, :content [{:tag :name, :content ["foo"]}]} {:tag :whitespace, :content [" "]} {:tag :symbol, :content [{:tag :name, :content ["bar"]}]} ")"]}]}]}

quote

foo

bar

Sjacket形式

テキストS式

• Clojureリーダで読み込むとそのまま書き戻せない

• コメントが削除される • インデントが崩れる • リーダマクロが展開される

• 読み込んだ後にそのまま書き戻せる中間形式へパース(Sjacket)

parse

analyze

render

source code

object format

• 解析器(symbol-analyzer)

• ハイライターのコア部分

• コード中のシンボルの使われ方を解析する独立したライブラリ

• 詳細は後述

parse

analyze

render

source code

object format

• 解析器の解析結果を付加した中間形式を書き出す

• ハイライトのルールをマップで定義できる

parse

analyze

render

source code

object format

; ハイライトのルールはマップで定義される(def rainbow-parens-rule (let [colors (atom (cycle (range 31 38))) color-stack (atom nil) open-fn (fn [x v] (let [[color] @colors] (swap! colors rest) (swap! color-stack conj color) (color-ansi color v))) close-fn (fn [x v] (let [[color] @color-stack] (swap! color-stack rest) (color-ansi color v)))] {:list {:open open-fn, :close close-fn} :vector {:open open-fn, :close close-fn} :map {:open open-fn, :close close-fn}}))!; ルール同士の合成も簡単にできる(compound-rules r1 r2)

• 解析器の解析結果を付加した中間形式を書き出す

• ハイライトのルールをマップで定義できる

parse

analyze

render

source code

object format

symbol-analyzer

symbol-analyzer

• コード中のシンボルの使われ方を解析する

• マクロを展開しつつ、構文木をトラバースするcode walkerとして構築

問題マクロ展開前

問題(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])

マクロ展開前

問題(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])

(let* [reader (reader “foobar.txt”)] (try [(read reader) (read reader)] (finally (.close reader))))

マクロ展開前

マクロ展開後

問題(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])

(let* [reader (reader “foobar.txt”)] (try [(read reader) (read reader)] (finally (.close reader))))

マクロ展開前

マクロ展開後

問題(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])

(let* [reader (reader “foobar.txt”)] (try [(read reader) (read reader)] (finally (.close reader))))

マクロ展開前

マクロ展開後

問題(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])

(let* [reader (reader “foobar.txt”)] (try [(read reader) (read reader)] (finally (.close reader))))

マクロ展開前

マクロ展開後

問題(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])

(let* [reader (reader “foobar.txt”)] (try [(read reader) (read reader)] (finally (.close reader))))

マクロ展開前

マクロ展開後

どのシンボルが展開前のどれに対応するのか!マクロ展開後の形だけ見ても分からない

テレメトリーメタファーTelemetry is the highly automated communications process by which measurements are made and other data collected at remote or inaccessible points and transmitted to receiving equipment for monitoring. (snip) Telemetry is used to study wildlife, and has been useful for monitoring threatened species at the individual level. Animals under study can be outfitted with instrumentation tags, which include sensors that measure temperature, diving depth and duration (for marine animals), speed and location (using GPS or Argos packages). “Telemetry” from Wikipedia

テレメトリーメタファーTelemetry is the highly automated communications process by which measurements are made and other data collected at remote or inaccessible points and transmitted to receiving equipment for monitoring. (snip) Telemetry is used to study wildlife, and has been useful for monitoring threatened species at the individual level. Animals under study can be outfitted with instrumentation tags, which include sensors that measure temperature, diving depth and duration (for marine animals), speed and location (using GPS or Argos packages). “Telemetry” from Wikipedia

野生動物にタグづけして自然に放し、生態を調査する

テレメトリーメタファーTelemetry is the highly automated communications process by which measurements are made and other data collected at remote or inaccessible points and transmitted to receiving equipment for monitoring. (snip) Telemetry is used to study wildlife, and has been useful for monitoring threatened species at the individual level. Animals under study can be outfitted with instrumentation tags, which include sensors that measure temperature, diving depth and duration (for marine animals), speed and location (using GPS or Argos packages). “Telemetry” from Wikipedia

野生動物にタグづけして自然に放し、生態を調査する

同様に、シンボルに目印をつけて!マクロ展開後の生態(使われ方)を調査する

どう実現するか?

メタデータ

• データにつけられる付加的な情報

• データに対する操作へは影響を与えない

• Clojureで使われる多くのデータ型につけられる

• シンボル、リスト、マップ、関数、etc.

メタデータuser=> ; シンボルfooに{:bar true}というメタデータをつけるuser=> (def x (with-meta ’foo {:bar true}))#’user/xuser=> xfoouser=> ; meta関数でメタデータを取得user=> (meta x){:bar true}user=> ; メタデータがついていても通常のシンボルと同じように使えるuser=> (symbol? x)trueuser=> ; リーダマクロ^を使うとリード時にメタデータをつけられるuser=> (def y ’^{:bar true}foo)#’user/yuser=> (meta y){:bar true}

解析ステップ1:マーキング

(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])!

(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])

ユニークなメタデータでシンボルをマーキング

解析ステップ2:抽出(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])!マクロ展開(let* [reader (reader “foobar.txt”)] (try [(read reader) (read reader)] (finally (.close reader))))マクロトップレベル変数

ローカル変数 トップレベル変数ローカル変数 トップレベル変数 ローカル変数

マクロ展開して特殊形式毎にパターンマッチで解析

抽出

解析ステップ3:アノテーション

(with-open [reader (reader “foobar.txt”)] [(read reader) (read reader)])

マクロ

トップレベル変数

ローカル変数 トップレベル変数

ローカル変数トップレベル変数 ローカル変数

解析結果をメタデータとして元のシンボルにつける

parse

analyze

render

source code

object format

Sjacket

Sjacket

S式

シンボル!情報

マーキング!・変換

抽出

アノテーション

基本的なAPI:extractメタデータでマーキングしたシンボルの使われ方を返すuser=> (extract '(let [^{:id 0} x 42] x)){0 {:type :local, :usage :def}}user=> (def x 42)#'user/xuser=> (extract '(let [^{:id 0} x ^{:id 1} x] [^{:id 2} x '^{:id 3} x])){0 {:type :local, :usage :def}, 1 {:type :var, :usage :ref, :var #'user/x}, 2 {:type :local, :usage :ref, :binding 0}, 3 {:type :quote},}user=> !!

; (let [x x]; [x ’x])

基本的なAPI:analyzeすべてのシンボルを対象にextractを呼び出す

基本的なAPI:analyzeすべてのシンボルを対象にextractを呼び出すuser=> (analyze-sexp '(let [x x] [x 'x]))

基本的なAPI:analyzeすべてのシンボルを対象にextractを呼び出すuser=> (analyze-sexp '(let [x x] [x 'x]))(let [x x] [x (quote x)])user=>

基本的なAPI:analyzeすべてのシンボルを対象にextractを呼び出すuser=> (analyze-sexp '(let [x x] [x 'x]))(let [x x] [x (quote x)])user=>user=> (set! *print-meta* true)niluser=>

基本的なAPI:analyze

基本的なAPI:analyzeuser=> (analyze-sexp '(let [x x] [x 'x]))

基本的なAPI:analyzeuser=> (analyze-sexp '(let [x x] [x 'x]))(^{:symbol-info {:type :macro, :macro #’clojure.core/let} :id 10} let [^{:symbol-info {:type :local, :usage :def}, :id 11} x ^{:symbol-info {:type :var, :usage :ref, :var #’user/x}, :id 12} x] [^{:symbol-info {:type :local, :usage :ref, :binding 11}, :id 13} x (^{:symbol-info {:op ^{:id 14} quote, :type :special}, :id 14} quote ^{:symbol-info {:type :quote}, :id 15} x)])user=>

symbol-analyzerの応用• シンタックスハイライトだけに限らず使えるケース考えられる

• マクロ展開しきったコンパイラに渡る直前の形ではなく、「ユーザがコードをどう書いたか」を知りたい場合もある

• コーディング規約チェッカー • コードのメトリクス計測

• マクロ定義への応用

マクロ定義への応用(defmacro defmacro/g! [name args & body] (let [syms (->> (flatten body) (filter g!-symbol?) distinct)] `(defmacro ~name ~args (let ~(-> (fn [s] `[~s (gensym ~(subs (name s) 2))]) (mapcat syms) vec) ~@body)))))

×安易なマクロ定義例

リテラルとしてのシンボル等も誤って含まれてしまう しかし、厳密にやるとコードウォーカーが必要で面倒

“Let Over Lambda”より defmacro/g!をClojureへ移植

マクロ定義への応用(defmacro defmacro/g! [name args & body] (let [syms (->> (flatten (analyze-sexp body)) (filter #(and (g!-symbol? %) (= (-> (meta %) :symbol-info :type) :none))) distinct)] `(defmacro ~name ~args (let ~(-> (fn [s] `[~s (gensym ~(subs (name s) 2))]) (mapcat syms) vec) ~@body)))))

◎symbol-analyzerを使ったより正確な定義例

parse

analyze

render

source code

object format

Sjacket

Sjacket

S式

シンボル!情報

マーキング!・変換

抽出

アノテーション

デモ

まとめ• マクロを認識するシンタックスハイライターと、そのコアで使われる解析器を紹介

• 解析器はシンタックスハイライトだけでなく、コードウォークが必要なマクロ定義等に応用可

• 今後の課題は、コーナーケースへの対応とそれに向けたClojureコンパイラの利用可否の検討

• genuine-highlighter

• https://github.com/athos/genuine-highlighter

• symbol-analyzer

• https://github.com/athos/symbol-analyzer