Использование API Semantic в Emacs для обработки исходных текстов

Semantic — часть CEDET, унифицированный API в Emacs для работы с исходными текстами. Semantic предоставляет прослойку высокого уровня над низкими процедурами лексического анализа исходников. В составе Semantic есть написанные на Emacs Lisp подобия lex(1) и yacc(1), так что чтобы добавить поддержку нового языка к Semantic, требуется описать его грамматику и определить некоторые специфичные для разбора сорцов на этом языке процедуры (документация по Semantic содержит всю необходимую информацию).

Состав API

На верхнем уровне своего API Semantic позволяет:

  • Запросить таблицу «тегов» для обрабатываемого файла («тег» в понимании Semantic — «значимый» элемент исходного кода — инклуд, определение функции, произвольный код на нулевом уровне вложенности и т. д.). Для каждого тега извлекается «название», расположение и какая-то дополнительная информация (например, для функций — список аргументов и т. д.)

  • Из полученной таблицы получить теги определённого типа (функции, глобальные переменные, классы), найти подходящий тег для автодополнения

  • Много всего другого. Инфраструктура Semantic Database позволяет сохранять результаты обработки не только текущего открытого файла, но и всех, от которых он зависит (заголовочные файлы стандартной библиотеки или просто другие файлы проекта, включённые в рассматриваемый при помощи #include ".." или import .. или (load "..") и т. д.)

  • Семантик позволяет скрывать/показывать тег (aka «folding»), устраивать всякое оформительство, искать теги рядом с курсором и т. д. и т. п.

Информация по указанному содержится в «Semantic Application Development Manual» (есть в дистрибутиве CEDET).

Где применяется

Большинству пользователей не нужно знание Semantic API: на его основе уже написано много полезный расширений для Emacs:

  • imenu, список тегов текущего файла

  • senator, высокоуровневая (не «пять строк вниз», а «следующий тег») навигация по тегам

  • speedbar умеет использовать Semantic для парсинга исходного текста

  • забавная фича — визуализация тегов текущего файла по сложности (количеству строк). По гистограмме видно, какая функция самая страшная (большая).

  • хороший комплишен для многих языков (и не просто по текущему буферу, а по включённым в файл, и даже тип переменных умеет показывать (правда, это всё ещё бывает buggy, особенно с не самыми популярными языками))

  • подсветка ошибочного синтаксиса (того, который не смогли распознать парсеры Semantic для данного языка) — такой облегчённый Flymake (Flymake использует непосредственно компилятор (или другое специальное средство проверки) языка и умеет показывать и более сложные ошибки и предупреждения (необъявленные переменные и прочее))

  • stickyfunc — когда в видимой части буфера видна лишь «нижняя» часть определения функции без заголовка, этот заголовок показывается в самой верхней строке буфера (сразу видно, чей «хвост» виднеется)

Ввод M-x semantic-load-enable-excessive-code-helpers в Emacs позволяет включить большинство вспомогательных функций для работы с кодом, написанных с помощью Semantic.

Эти штуки значительно помогают разобраться в чужом коде.

В CEDET вообще много плюшек, о существовании которых многие не подозревают — внимательно читаем офсайт.

Чуть более сложное использование инфраструктуры Semantic

(Описываемые дальше тексты программ на Elisp лежат у меня в репо).

Извлечение частей исходных текстов

Недавно мне потребовалось найти решение для такой задачи:

«Вывести на стандартный вывод тело функции с данным именем в данном файле»

В терминах Emacs Lisp с юзанием Семантика она решается так:

Структура решения понятна: get-file-tags запрашивает список тегов Семантика для обозначенного файла (в Semantic тег представлен простой списочной структурой, работа с которой должна осуществляться через стабильный API), get-tag-body возвращает (строкой) тело тега (в Semantic каждый тег знает, на каких позициях в файле находится его определение), print-tag-body обёртывает эти две функции.

Применяется на практике так (считаем, что grok-lisp.el содержит вышеприведённый код):

$ emacs --batch --load grok-lisp.el \
  --exec '(print-tag-from-file "some-tag" "some-source.scm")' \
  2> /dev/null

(Стандартный поток ошибок stderr специально перенаправляется в пустоту: в Emacs функция princ, используемая в решении, при запуске в пакетном режиме (--batch) печатает свой аргумент на стандартный вывод stdout, а на stderr плюётся функция message, при помощи которой Emacs может выводить статусные сообщения при загрузке файла или подключении библиотеки; например, у меня печатается такое:

!! File eieio uses old-style backquotes !!
Loading vc-hg...

Перенаправление 2> /dev/null удаляет из вывода консольной команды статусные высеры Емакса, не оставляя там ничего, кроме запрошенного тела функции.)

Забавно, что print-tag-from-file нормально работает со многими языками (тестировал с Elisp, Scheme, C, Python), а его реализация не содержит упоминаний хотя бы одного из них.

Построение графа зависимостей между функциями

Удивительно, но я не нашёл ни одной рисовалки зависимостей функций для сорцов на Scheme. Было бы интересно решить эту задачу при помощи Семантика (надеюсь, где-то всё-таки есть более продвинутый генератор графов зависимостей).

Прежде всего, условимся считать функцию f зависящей от g, если в теле f упоминается символ g. Это дико провокационная формулировка, не учитывающая некоторые поганенькие подробности. Например, глобальный биндинг функции g может быть перекрыт совершенно левым локальным g; функция f будет упоминать g, но не являться зависящей от той самой функции g из глобальной области видимости, которую мы имеем в виду:

(define (g x) (* x x))

(define (f x)
  (let ((g 5))
    (+ g x)))

или так:

(define (g x) 5)

(define (f x)
  (define (g x) (/ x 2))
  (+ 10 (g x)))

Решение задачи в общем виде для произвольного языка (в смысле, для хотя бы нескольких одновременно) нетривиально.

У меня всё было проще — есть пяток файлов с маленькими функциями, надо нарисовать между ними грамотно стрелочки.

Задача разбивается на несколько частей.

Нужно получить список всех функций в нужном файле. Кроме того, файл может использовать функции из других подгружаемых файлов (например, в Scheme я пишу (load "shared.scm") и использую функции оттуда) — их тоже нужно запросить.

Теперь нужно обработать целиком определение какого-то тега и найти в нём вхождения других. Обычный поиск по тексту не подойдёт, лучше использовать предоставляемый Семантиком лексер — через функцию semantic-lex. Тогда легко видеть, что вот такая функция делает что надо:

Эта функция принимает два параметра — тег Семантика и таблицу тегов, которые предполагается искать в первом (обёртка над этой функцией, формирующая два нужных аргумента tag и tag-table, будет описана далее).

Дальше строится ассоциативный массив из tag-table, где ключами являются метки искомых в функции тегов.

После этого при помощи (semantic-lex from to 1.0e+INF) строится список из лексем в теле функции, из них выбираются те, которые имеют тип 'symbol (для Лиспов это так) и присутствуют в таблице искомых тегов. Число 1.0e+INF, как ни странно, обозначает бесконечность, а вызов semantic-lex с таким аргументом означает парсинг без ограничения на (глубину (вложенности (блока))). Если в обрабатываемой функции встретился искомый символ, он добавляется к списку result, который является возвращаемым из get-tag-deps значением.

Функцию get-tag-deps невозможно ещё использовать, понадобится ещё обёртка:

Которая строит список зависимостей не для одной данной функции, а для всех в указанном файле, возвращая список следующей структуры:

((TAG1 . DEPS1) (TAG2 . DEPS2) .. (TAGN . DEPSN))

Где DEPS — тоже списки из тегов; очевидно, что множество первых элементов пар является подмножеством вторых элементов (так как функция может «зависеть» от функций, объявленных в другом файле).

Осталось написать ещё одну обёртку, которая будет действовать уже на уровне списка файлов, выводя на печать описание графа зависимостей между функциями в формате dot (возможно, правильнее было бы использовать высокоуровневую систему eieio (аналог CLOS для Elisp), а из описаний в её терминах уже генерить текстовый вывод в различных форматах, но dot — это тоже очень переносимый и широко используемый формат для описания графов):

Эта функция вкладывает несколько отображений разных списков друг в друга: в каждом файле из переданного ей списка для каждой зависимости каждой функции этого файла (почти сошёл с ума, пока писал эту конструкцию со словами «каждый» :-) выводит строку вида "function" -> "dependency".

В предположении, что весь вышеперечисленный код содержится в файле grok-lisp.el, можно попробовать использовать его для генерации графа зависимостей между функциям в файле на каком-нибудь Лиспе. Например, в том же grok-lisp.el :-)

$ emacs --batch --load grok-lisp.el \
  --exec '(print-files-depgraph "grok-lisp.el")' \
  2> /dev/null \
  > grok-lisp.dot

Заглянув в полученных файл, увидим:

Теперь любой программой из Graphviz можно сгенерировать изображение:

$ fdp -Tpng -O grok-lisp.dot

А вот и получившийся граф:

grok-lisp.dot

Эта программа уже не работает, скажем, с исходниками на C. Например, придётся к списку возможных типов лексемы в get-tag-deps добавлять 'NAME, потому что Semantic использует для сишных сорцов именно такое обозначение (а не 'symbol, как для Лиспов). Кроме того, может понадобиться изменять режим обработки semanticdb включаемых в сорец файлов. Мне к примеру, более интересно смотреть лексические зависимости между теми функциями, которые написал я, а не то, сколько функций libc для работы со строками упоминает код (можно их считать элементарными операциями).

Описанный код также подавится, если в нескольких файла встречается функция с одним и тем же именем :-)

eieio содержит программку call-tree, которая позволяет генерировать графы вызовов для функций на Elisp; рассмотренное выше решение, однако, занимается созданием графов простой лексической зависимости между функциями. Я успешно применял print-files-depgraph и для генерации развесистой клюквы из программ на Scheme.

Использование dot в качестве выходного формата позволяет использовать graph(3) для обработки программы (из графа зависимостей, очевидно, можно узнать очень многое; хотя бы уже один взгляд на граф программы даёт возможность оценить её структуру, связность и другое).

git