Сайт Димы Джуса / блог / …

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

3 ноября 2007
Теги: , , , , , .

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

Состав API

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

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

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

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

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

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

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

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

(Описываемые дальше тексты программ на Elisp лежат у меня в репо, однако со временем их содержание в репозитории может измениться, поэтому лучше смотреть ещё и последнюю версию).

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

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

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

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

(load-file "~/.emacs.d/cedet/common/cedet.el")
(semanticdb-toggle-global-mode)

;; Return a Semantic tag table for file
(defun get-file-tags (file-name)
  (with-current-buffer 
      (find-file-noselect file-name)
    (semantic-fetch-tags)))

;; Return full tag source code (suitable for princ-ing)
(defun get-tag-body (tag)
  (let ((from (semantic-tag-start tag))
        (to (semantic-tag-end tag))
        (buffer (semantic-tag-buffer tag)))
    (with-current-buffer buffer
      (buffer-substring from to))))  

;; Print body of tag with specified name from specified file
(defun print-tag-from-file (tag-name file-name)
  (let ((tag-table (get-file-tags file-name)))
    (princ (format "%s" 
                   (get-tag-body 
                    (semantic-find-first-tag-by-name 
                     tag-name 
                     tag-table))))))

Структура решения понятна: 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 удаляет из вывода консольной команды статусные высеры Емакса, не оставляя там ничего, кроме запрошенного тела функции.)

Самая первая строчка может понадобиться, если CEDET установлен в пользовательскую директорию; в противном случае её можно убрать.

Забавно, что 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") и использую функции оттуда) — их тоже нужно запросить.

(require 'semantic)
(require 'semanticdb)
(semanticdb-toggle-global-mode)

;; Get a list of all 'function tags declared in specified file and its
;; included files
(defun get-file-functions-deep (file-name)
  (with-current-buffer
      (find-file-noselect file-name)
    (semanticdb-strip-find-results
     (semanticdb-find-tags-by-class
      'function))))

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

;; Return a list of tags from tag-table which are also mentioned in
;; tag
(defun get-tag-deps (tag tag-table)
  (let ((from (semantic-tag-start tag))
        (to (semantic-tag-end tag))
        (buffer (semantic-tag-buffer tag))
        ;; Build associative list with tag names as keys
        (deps (mapcar 
               (lambda (tag) 
                 (cons (semantic-tag-name tag)
                       tag))
               tag-table)))
    (with-current-buffer buffer
      (let (result)
        ;; cddddr is a Lisp-oriented hack to prevent tag itself from
        ;; inclusion to dependency list
        (dolist (lexem (cddddr (semantic-lex from to 1.0e+INF)) result)
          (if (eq 'symbol (car lexem))
              (let* ((lexem-string (buffer-substring 
                                    (cadr lexem)
                                    (cddr lexem)))
                     (found-tag (assoc lexem-string
                                       deps)))
                (if found-tag
                    (add-to-list 'result (cdr found-tag) t)))))))))

Эта функция принимает два параметра — тег Семантика и таблицу тегов, которые предполагается искать в первом (обёртка над этой функцией, формирующая два нужных аргумента 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 невозможно ещё использовать, понадобится ещё обёртка:

;; Return a list of pairs (TAG . DEPS) where DEPS is a list of
;; functions TAG «depends» on
(defun get-file-depgraph (file-name)
  (interactive "fFile name: ")
  (let ((deep-tag-table (get-file-functions-deep file-name))
        (file-tag-table (get-file-functions file-name))
        (depgraph))
    (dolist (tag file-tag-table depgraph)
      (let ((deps (get-tag-deps tag deep-tag-table)))
        (add-to-list 'depgraph (cons tag deps) t)))))

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

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

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

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

;; Print depgraph for functions in specified files in DOT format
;; (suitable for processing with Graphviz programs)
(defun print-files-depgraph (&rest file-names)
  (princ "digraph D {\n")
  (princ "overlap=scale;\n")
  (dolist (file file-names)
    (let ((depgraph (get-file-depgraph file)))
      (dolist (dep-list-for-tag depgraph)
        (let ((function-name (semantic-tag-name 
                              (car dep-list-for-tag))))
          (princ (format "\"%s\";\n" function-name))
          (dolist (dependency (cdr dep-list-for-tag))
            (princ (format "\"%s\" -> \"%s\";\n"
                     (semantic-tag-name dependency)
                     function-name)))))))
  (princ "}\n"))

Эта функция вкладывает несколько отображений разных списков друг в друга: в каждом файле из переданного ей списка для каждой зависимости каждой функции этого файла (почти ебанулся, пока писал эту конструкцию со словами «каждый» :-) выводит строку вида "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

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

digraph D {
    overlap=scale;
    "get-file-tags";
    "get-tag-body";
    "get-tag-deps";
    "print-tag-from-file";
    "get-file-tags" -> "print-tag-from-file";
    "get-tag-body" -> "print-tag-from-file";
    "get-file-functions";
    "get-file-tags" -> "get-file-functions";
    "get-file-functions-deep";
    "get-file-depgraph";
    "get-file-functions-deep" -> "get-file-depgraph";
    "get-file-functions" -> "get-file-depgraph";
    "get-tag-deps" -> "get-file-depgraph";
    "print-files-depgraph";
    "get-file-depgraph" -> "print-files-depgraph";
}

Теперь любой программой из 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) для обработки программы (из графа зависимостей, очевидно, можно узнать очень многое; хотя бы уже один взгляд на граф программы даёт возможность оценить её структуру, связность и другое).

Комментарии:

mv, 18.11.2007

Занимательно... Спасибо.

Оставить комментарий:





(3∙2+7)∙x=117,

Бесконечно много решений? F5!

← Снова про TeX Live Homo+ →