38.1 Грамматика языка Tree-sitter

Загрузка грамматики языка

Tree-sitter использует грамматику языка для разбора текста на этом языке. В Emacs грамматика языка представлена символом. Например, грамматика языка C представлена символом c, и c может быть передана функциям tree-sitter в качестве аргумента language.

Грамматики языков Tree-sitter распространяются в виде динамических библиотек. Чтобы использовать грамматику языка в Emacs, необходимо убедиться, что динамическая библиотека установлена в системе. Emacs ищет грамматики языков в нескольких местах, в следующем порядке:

В каждом из этих каталогов 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 может содержать дополнительные сведения об ошибке.

Функция: treesit-language-available-p language &optional detail

Эта функция возвращает не 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")

для языка, который считает себя слишком «крутым», чтобы соблюдать соглашения.

Функция: treesit-library-abi-version &optional min-compatible

Эта функция возвращает версию двоичного интерфейса приложений (ABI) грамматики языка, поддерживаемую библиотекой tree-sitter. По умолчанию она возвращает последнюю версию ABI, поддерживаемую библиотекой, но если min-compatible не равен nil, она возвращает самую старую версию ABI, которую библиотека всё ещё может поддерживать. Библиотеки грамматик языков должны быть собраны для версий ABI между самой старой и самой новой версиями, поддерживаемыми библиотекой tree-sitter, иначе библиотека не сможет их загрузить.

Функция: treesit-language-abi-version language

Эта функция возвращает версию 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 также поставляется с «режимом инспекции», который отображает информацию об узлах в точке в строке режима.

Команда: treesit-explore-mode

Этот режим открывает окно с отображением синтаксического дерева исходного кода в текущем буфере. Выделение текста в буфере с исходным кодом подсвечивает соответствующие узлы в отображении синтаксического дерева. Нажатие на узлы в синтаксическом дереве подсвечивает соответствующий текст в буфере с исходным кодом.

Команда: treesit-inspect-mode

Этот дополнительный режим отображает в строке режима узел, который начинается в точке. Например, строка режима может отображать

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».