Tree-sitter использует грамматику языка для разбора текста на этом языке. В Emacs грамматика языка представлена символом. Например, грамматика языка C представлена символом c, и c может быть передана функциям tree-sitter в качестве аргумента language.
Грамматики языков Tree-sitter распространяются в виде динамических библиотек. Чтобы использовать грамматику языка в Emacs, необходимо убедиться, что динамическая библиотека установлена в системе. Emacs ищет грамматики языков в нескольких местах, в следующем порядке:
treesit-extra-load-path;
user-emacs-directory (см. Файл инициализации);
В каждом из этих каталогов Emacs ищет файл с расширениями, заданными переменной dynamic-library-suffixes.
Если Emacs не может найти библиотеку или испытывает проблемы с её загрузкой, он вызывает ошибку treesit-load-language-error. Данные этой ошибки могут быть следующими:
(not-found error-msg …)Это означает, что Emacs не смог найти библиотеку грамматики языка.
(symbol-error error-msg)Это означает, что Emacs не смог найти в библиотеке ожидаемую функцию, которую должна экспортировать каждая библиотека грамматики языка.
(version-mismatch error-msg)Это означает, что версия библиотеки грамматики языка несовместима с версией библиотеки tree-sitter.
Во всех этих случаях error-msg может содержать дополнительные сведения об ошибке.
Эта функция возвращает не nil, если грамматика для language существует и может быть загружена.
Если detail не равен nil, возвращает (t . nil), когда language доступен, и (nil . data), когда недоступен. data — это данные сигнала treesit-load-language-error.
По соглашению, имя файла динамической библиотеки для language имеет вид libtree-sitter-language.ext, где ext — системно-зависимое расширение для динамических библиотек. Также по соглашению функция, предоставляемая этой библиотекой, называется tree_sitter_language. Если библиотека грамматики языка не следует этому соглашению, следует добавить запись
(language library-base-name function-name)
в список в переменной treesit-load-name-override-list, где library-base-name — базовое имя файла динамической библиотеки (обычно, libtree-sitter-language), а function-name — функция, предоставляемая библиотекой (обычно, tree_sitter_language). Например,
(cool-lang "libtree-sitter-coool" "tree_sitter_cooool")
для языка, который считает себя слишком «крутым», чтобы соблюдать соглашения.
Эта функция возвращает версию двоичного интерфейса приложений (ABI) грамматики языка, поддерживаемую библиотекой tree-sitter. По умолчанию она возвращает последнюю версию ABI, поддерживаемую библиотекой, но если min-compatible не равен nil, она возвращает самую старую версию ABI, которую библиотека всё ещё может поддерживать. Библиотеки грамматик языков должны быть собраны для версий ABI между самой старой и самой новой версиями, поддерживаемыми библиотекой tree-sitter, иначе библиотека не сможет их загрузить.
Эта функция возвращает версию ABI библиотеки грамматики языка, загруженной Emacs для language. Если language недоступен, функция возвращает nil.
Синтаксическое дерево — это то, что генерирует парсер. В синтаксическом дереве каждый узел представляет фрагмент текста и связан с другими узлами отношением «родитель-потомок». Например, если исходный текст
1 + 2
его синтаксическое дерево может выглядеть так:
+--------------+
| root "1 + 2" |
+--------------+
|
+--------------------------------+
| expression "1 + 2" |
+--------------------------------+
| | |
+------------+ +--------------+ +------------+
| number "1" | | operator "+" | | number "2" |
+------------+ +--------------+ +------------+
Мы также можем представить его в виде s-выражения:
(root (expression (number) (operator) (number)))
Имена, такие как root, expression, number и operator, определяют тип узлов. Однако не все узлы в синтаксическом дереве имеют тип. Узлы, которые не имеют типа, известны как анонимные узлы, а узлы с типом — именованные узлы. Анонимные узлы — это токены с фиксированным написанием, включая знаки пунктуации, например скобку ‘]’, и ключевые слова, такие как return.
Чтобы упростить анализ синтаксического дерева, многие грамматики языков присваивают имена полей дочерним узлам. Например, узел function_definition может иметь поле declarator и body:
(function_definition declarator: (declaration) body: (compound_statement))
Чтобы облегчить понимание синтаксиса языка и отладку Lisp-программ, использующих синтаксическое дерево, Emacs предоставляет режим «исследования», который отображает синтаксическое дерево исходного кода в текущем буфере в реальном времени. Emacs также поставляется с «режимом инспекции», который отображает информацию об узлах в точке в строке режима.
Этот режим открывает окно с отображением синтаксического дерева исходного кода в текущем буфере. Выделение текста в буфере с исходным кодом подсвечивает соответствующие узлы в отображении синтаксического дерева. Нажатие на узлы в синтаксическом дереве подсвечивает соответствующий текст в буфере с исходным кодом.
Этот дополнительный режим отображает в строке режима узел, который начинается в точке. Например, строка режима может отображать
parent field: (node (child (…)))
где node, child и т. д. — это узлы, которые начинаются в точке. parent — это родитель node. node отображается жирным шрифтом. field-name — это имена полей node, child и т. д.
Если в точке не начинается ни один узел, т. е. точка находится внутри узла, то строка режима отображает самый ранний узел, охватывающий точку, и его непосредственного родителя.
Этот дополнительный режим не создаёт парсеры самостоятельно. Он использует первый парсер в (treesit-parser-list) (см. Использование парсера Tree-sitter).
Авторы грамматик языков определяют грамматику языка программирования, которая определяет, как парсер строит конкретное синтаксическое дерево из текста программы. Чтобы эффективно использовать синтаксическое дерево, вам необходимо обратиться к файлу грамматики.
Файл грамматики обычно называется grammar.js в репозитории проекта грамматики языка. Ссылку на домашнюю страницу грамматики языка можно найти на домашней странице tree-sitter.
Определение грамматики написано на JavaScript. Например, правило, соответствующее узлу function_definition, может выглядеть так:
function_definition: $ => seq(
$.declaration_specifiers,
field('declarator', $.declaration),
field('body', $.compound_statement)
)
Правила представлены функциями, которые принимают единственный аргумент $, представляющий всю грамматику. Сама функция конструируется с помощью других функций: функция seq объединяет последовательность дочерних элементов; функция field аннотирует дочерний элемент именем поля. Если записать приведённое выше определение в так называемой форме Бэкуса-Наура (BNF), оно будет выглядеть так:
function_definition := <declaration_specifiers> <declaration> <compound_statement>
и узел, возвращаемый парсером, будет выглядеть так:
(function_definition (declaration_specifier) declarator: (declaration) body: (compound_statement))
Ниже приведён список функций, которые можно встретить в определении грамматики. Каждая функция принимает другие правила в качестве аргументов и возвращает новое правило.
seq(rule1, rule2, …)соответствует каждому правилу последовательно одно за другим.
choice(rule1, rule2, …)соответствует одному из правил в своих аргументах.
repeat(rule)соответствует rule ноль или более раз. Это аналогично оператору ‘*’ в регулярных выражениях.
repeat1(rule)соответствует rule один или более раз. Это аналогично оператору ‘+’ в регулярных выражениях.
optional(rule)соответствует rule ноль или один раз. Это аналогично оператору ‘?’ в регулярных выражениях.
field(name, rule)присваивает имя поля name дочернему узлу, соответствующему rule.
alias(rule, alias)заставляет узлы, соответствующие rule, отображаться как alias в синтаксическом дереве, генерируемом парсером. Например,
alias(preprocessor_call_exp, call_expression)
заставляет любой узел, соответствующий preprocessor_call_exp, отображаться как call_expression.
Ниже приведены функции грамматики, имеющие меньшее значение для чтения грамматики языка.
token(rule)помечает rule для создания одного листового узла. То есть вместо создания родительского узла с отдельными дочерними узлами под ним, всё объединяется в один листовой узел. См. Получение узлов.
token.immediate(rule)Обычно правила грамматики игнорируют предшествующие пробельные символы; это заставляет rule соответствовать только тогда, когда перед ним нет пробельных символов.
prec(n, rule)присваивает rule приоритет уровня n.
prec.left([n,] rule)помечает rule как левоассоциативный, опционально с уровнем n.
prec.right([n,] rule)помечает rule как правоассоциативный, опционально с уровнем n.
prec.dynamic(n, rule)это аналогично prec, но приоритет применяется во время выполнения, а не во время компиляции.
В документации проекта tree-sitter есть подробнее о написании грамматики. Прочтите особенно раздел «The Grammar DSL».