Conao3 Note

Clojureで始めるTree-sitter


東京Emacs勉強会オクトーバーフェスティバル2024の登壇資料です。

自己紹介 - Conao3

attrorg: :width 300 img

アジェンダ

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 を入れる。

Terminal window
language=conao3-clojure
mkdir tree-sitter-${language}
cd tree-sitter-${language}
npm init
npm install tree-sitter-cli --save-dev

tree-sitter init

initする。

Terminal window
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

文法ファイルからパーサを生成する。

Terminal window
npx tree-sitter generate

Cで書かれたパーサが生成される。

Terminal window
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

使ってみる。

Terminal window
echo hello > tmp
npx tree-sitter parse tmp

このような結果が表示される

Terminal window
(source_file [0, 0] - [1, 0])

tree-sitter test

テストランナーが付属している。

test/corpus/basic.txt を以下の内容で用意する。

Terminal window
=====
Basic
=====
hello
---
(source_file)

実行

Terminal window
npx tree-sitter test

結果

Terminal window
basic:
1. Basic

実際のところ、テストを実行するためには事前にパーサを生成しておく必要があるので、連続で実行するようにしておくと良い。

Terminal window
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.14
a
\a
nil
true 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 repository
Compiling library
Library 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 "; ")

まとめ

告知

Clojure学んでみたい。。?本を書きました!! 11/2 (来週日曜日) 池袋です。 「技術書典17 clojure」で検索してみてください。

Clojureの標準モジュールであるclojure.coreを網羅的に解説しました。ぜひお手に取ってみてください!