Clojureで始めるTree-sitter
東京Emacs勉強会オクトーバーフェスティバル2024の登壇資料です。
自己紹介 - Conao3
attrorg: :width 300
-
転職しました!
- 広島 → 目黒
- Pythonista → Clojurian
- せっかく上京してきたので遊んでくれる人募集してます!
- 具体的にはVim-jpの #event-ramen で「ラーメン行きたい」と呟くと召喚できます!
-
趣味
- EmacsLisp
- Author: leaf, seml-mode, org-generate, keg,,,
- Co-maintainer: ddskk, cask,,,
- EmacsLisp
-
興味
- Lisp, Type, Compiler, GraphQL
アジェンダ
-
Emacs meets tree-sitter
-
tree-sitterパーサの作り方
-
tree-sitterパーサをEmacsから使う
この記事で紹介するtreesitterパーサ及びEmacsの設定は github.com/conao3-playground/tree-sitter-conao3-clojureに置いてあります。
Emacs meets tree-sitter
Emacs 29でtree-sitterサポートがビルトインになった。
tree-sitterの登場により、エディタそれぞれでハイライトの実装をしなくてよい。 ある種、ハイライト分野のLSP。
速い!!
なお、tree-sitter.elはtreesit.elとしてビルトインされたので、ネット上の情報を調べるのはこの知識があると調べやすい。
tree-sitterパーサの作り方
tree-sitter/creating-parser が丁寧に書いてある。
tree-sitter-cliのインストール
まず対象の言語を決めて、 tree-sitter-cli
を入れる。
language=conao3-clojure
mkdir tree-sitter-${language}cd tree-sitter-${language}
npm initnpm install tree-sitter-cli --save-dev
tree-sitter init
initする。
npx tree-sitter init
grammar.jsが文法ファイル。生成時点はこのような内容。
module.exports = grammar({ name: "conao3_bash",
rules: { // TODO: add the actual grammar rules source_file: $ => "hello" }});
tree-sitter generate
文法ファイルからパーサを生成する。
npx tree-sitter generate
Cで書かれたパーサが生成される。
new file: src/grammar.json new file: src/node-types.json new file: src/parser.c new file: src/tree_sitter/alloc.h new file: src/tree_sitter/array.h new file: src/tree_sitter/parser.h
tree-sitter parse
使ってみる。
echo hello > tmpnpx tree-sitter parse tmp
このような結果が表示される
(source_file [0, 0] - [1, 0])
tree-sitter test
テストランナーが付属している。
test/corpus/basic.txt
を以下の内容で用意する。
=====Basic=====
hello
---
(source_file)
実行
npx tree-sitter test
結果
basic: 1. ✓ Basic
実際のところ、テストを実行するためには事前にパーサを生成しておく必要があるので、連続で実行するようにしておくと良い。
npx tree-sitter generate && npx tree-sitter test
Clojureの文法ファイル
Clojureをパースするために書いたのはこちら。
module.exports = grammar({ name: "conao3_clojure",
rules: { source_file: $ => repeat($._form),
_form: $ => choice( $.list, $.vector, $.map, $.set,
$.string, $.number, $.symbol, $.character, $.nil, $.boolean, $.keyword, ),
list: $ => seq("(", repeat($._form), ")"), vector: $ => seq("[", repeat($._form), "]"), map: $ => seq("{", repeat($._form), repeat($._form), "}"), set: $ => seq("#{", repeat($._form), "}"),
string: $ => /"[^"]*"/, number: $ => token(seq(/[+-]?/, choice(/\d+/, /\d*\.\d+/))), symbol: $ => /'[^()\[\]{}'"\s]+/, character: $ => /\\./, nil: $ => "nil", boolean: $ => choice("true", "false"), keyword: $ => /:[^()\[\]{}'"\s]+/, },});
簡略化されているが、それでもこれくらいのものはパースできるようになっている。
=====Basic=====
"a"42 3.14a\aniltrue false:a
---
(source_file (string) (number) (number) (symbol) (character) (nil) (boolean) (boolean) (keyword))
===========Collections===========
(a b c)[a b c]{:a 1 :b 2 :c 3}#{a b c}
---
(source_file (list (symbol) (symbol) (symbol)) (vector (symbol) (symbol) (symbol)) (map (keyword) (number) (keyword) (number) (keyword) (number)) (set (symbol) (symbol) (symbol)))
tree-sitterパーサをEmacsから使う
設定
(leaf *treesit :custom ((treesit-font-lock-level . 4)) :config (require 'treesit)
(defvar conao3-clojure-ts-mode--indent-rules `((conao3-clojure ((parent-is "list") (nth-sibling 1) define))))
(define-derived-mode conao3-clojure-ts-mode prog-mode "[conao3]Clojure" (unless (treesit-language-available-p 'conao3-clojure) (treesit-install-language-grammar 'conao3-clojure))
(setq treesit-primary-parser (treesit-parser-create 'conao3-clojure))
;; ここでいろいろ準備する
(treesit-major-mode-setup))
(add-to-list 'auto-mode-alist '("\\.clj[sc]?\\'" . clojure-mode)) (add-to-list 'auto-mode-alist '("\\.edn\\'" . clojure-mode)) (add-to-list 'major-mode-remap-alist '(clojure-mode . conao3-clojure-ts-mode)) (add-to-list 'treesit-language-source-alist '(conao3-clojure "https://github.com/conao3-playground/tree-sitter-conao3-clojure")))
こんな感じで書く。
使い方
このメジャーモードを実行する。具体的には以下の内容を記述した適当なcljファイルを開く。
"a"(a b c){:a 1 :b 2 :c 3}
そうするとこのような感じで出力され、自動的にtree-sitterパーサのclone及びコンパイル、インストールが行われる。便利。
Cloning repositoryCompiling libraryLibrary installed to /Users/conao/.debug.emacs.d/eglot-clojure-lsp/tree-sitter/libtree-sitter-conao3-clojure.dylib
Emacsがtree-sitterパーサによってどのように理解しているかは M-x treesit-explore-mode
を見ると分かりやすい。
treesit-major-mode-setup
treesit-major-mode-setup
が便利関数。 ドキュメントを要約するとこんなことをするらしい。
;; `treesit-font-lock-settings';; -> set up fontification and enable `font-lock-mode'.
;; `treesit-simple-indent-rules';; -> set up indentation.
;; `treesit-defun-type-regexp' or `defun' is defined in `treesit-thing-settings';; -> set up `beginning-of-defun-function' and `end-of-defun-function'.
;; `treesit-defun-name-function';; -> set up `add-log-current-defun'.
;; `treesit-simple-imenu-settings';; -> set up Imenu.
;; If either `treesit-outline-predicate' or `treesit-simple-imenu-settings';; -> setup Outline minor mode.
;; If `sexp', `sentence' are defined in `treesit-thing-settings';; -> enable tree-sitter navigation commands for them.
これらの変数を treesit-major-mode-setup
の前に設定しておくと、関連する設定をいい感じにやってくれて便利
コードハイライト
これを追加する。
(setq-local treesit-font-lock-settings (treesit-font-lock-rules :language 'conao3-clojure :feature 'all '(((keyword) @font-lock-keyword-face) ((string) @font-lock-string-face) ((number) @font-lock-constant-face) ((symbol) @font-lock-function-name-face) ((boolean) @font-lock-builtin-face) ((nil) @font-lock-constant-face))))
(setq-local treesit-font-lock-feature-list '((all)))
インデント
これを追加する
(setq-local treesit-simple-indent-rules '((conao3-clojure ((parent-is "list") parent-bol 1) ((parent-is "vector") parent-bol 1) ((parent-is "map") parent-bol 1) ((parent-is "set") parent-bol 1) (no-node parent-bol 0))))
thing-at-point
treesit-defun-type-regexp
or defun
is defined in treesit-thing-settings
-> set up beginning-of-defun-function
and end-of-defun-function
.
わかりませんでした!!
treesit-defun-name-function
わかりませんでした!! そもそも add-log-current-defun
って何?
iMenu
バッファの中で重要そうな行を抽出する機能。 これを追加する。詳細は要調査。
(setq-local treesit-simple-imenu-settings '(("List" "\\`list\\'" nil nil) ("Vector" "\\`vector\\'" nil nil) ("Map" "\\`map\\'" nil nil) ("Set" "\\`set\\'" nil nil)))
Outline minor mode
treesit-simple-imenu-settings
を設定していれば outline-minor-mode
で使えるようになるらしい。
M-x outline-minor-mode
, M-x outline-cycle
コメント
M-;
でコメント追加するやつ。 これを追加する。
(setq comment-start "; ")
まとめ
- tree-sitter (treesit) により、エディタに依存したそれぞれのパーサを作るのではなく、「言語のパーサ」として異なるエディタのユーザーが共同で作りあげられるようになった。
- tree-sitterパーサを作るのは (簡単には) 簡単に作れるので、やってみると便利
- そのtree-sitterパーサに依存したメジャーモードを作るのも簡単
- ぜひやってみてください
告知
Clojure学んでみたい。。?本を書きました!! 11/2 (来週日曜日) 池袋です。 「技術書典17 clojure」で検索してみてください。
Clojureの標準モジュールであるclojure.coreを網羅的に解説しました。ぜひお手に取ってみてください!