Как стать автором
Обновить

Создание режима GNU Emacs для C-подобного языка

Уровень сложностиСложный
Время на прочтение6 мин
Количество просмотров1K

Недавно я разработал ещё один режим GNU Emacs для C-подобного языка программирования C2. Если в предыдущий раз для другого C-подобного языка я написал код с нуля, то в этот раз решил воспользоваться возможностью так называемого наследования режимов. В этой статье хочу поделиться с вами как это делается, и что у меня из этого вышло. (Предполагается, что читатель ознакомился с материалом предыдущей статьи Как написать свой режим для GNU Emacs и опубликовать его в MELPA или имеет собственный уникальный опыт разработки режимов GNU Emacs.)

Фрагмент кода режима и подсветка C2
Фрагмент кода режима и подсветка C2

Введение

Как рассказывалось в предыдущей статье, я уже пытался использовать наследование режимов для C-подобного языка. Однако в тот раз из этого ничего хорошего не вышло. Основная причина была в том, что тот язык не использовал символ точки с запятой в качестве признака завершения некоторых конструкций, что приводило к неверной расстановке отступов при переходе на новую строку. Тогда мне пришлось сделать полное описание синтаксиса языка и написать ту самую функцию, обеспечивавшую правильность отступов.

Язык C2 позиционируется как эволюция языка C. Наиболее заметным отличием является отказ от препроцессора с его #include и #define. Есть и другие новшества, за которыми отсылаю читателя к документации. Также рекомендую обратиться к примерам кода и тестам в коде его компилятора.

Язык C2 сохранил обязательность точки с запятой, что вселяло надежду на быструю реализацию режима GNU Emacs. Однако, оказалось, что следующая простая конструкция работает, но не так как надо:

;;;###autoload
(define-derived-mode c2-mode c-mode "C2"
  "Major mode for editing code written in the C2 Programming Language.")

Проблема была в том, что в этом случае файлы C2 воспринимаются как файлы C! Соответствено и обрабатываются они как файлы C, и все режимы настроенные для C будут запускаться для C2. А значит мы увидим море проблем и ложных сообщений об ошибках в коде на C2. Непорядок!

Тогда я решил ознакомиться с кодом режимов для других C-подобных языков, чтобы разобраться, а как они были реализованы? Вот эти языки и их режимы: D, Dart, C#, Go, JavaScript, Kotlin, Rust. Смотрел также в код встроенных режимов для C++, Java, Perl. И оказалось всё очень неоднозначно! Во-первых, лишь часть режимов использовали это самое наследование от C. Во-вторых, это наследование -- ну такое...

В общем, пришлось мне изрядно попотеть. Ещё одну проблему подбрасывал сам C2. Оказалось, что для него нет грамматики! А значит за описанием его конструкций нужно идти в документацию. Анализ примеров из кода компилятора навёл на мысль, что между документацией и тестами есть расхождение... Ну да ладно.

Структура кода режима

Рассказ будет вестись с примерами кода из написанного мною режима.

В самом начале кода режима производим загрузку необходимых модулей:

(require 'cc-bytecomp)
(require 'cc-fonts)
(require 'cc-langs)
(require 'cc-mode)
(require 'compile)

(eval-when-compile
  (let ((load-path (if (and (boundp 'byte-compile-dest-file)
                            (stringp byte-compile-dest-file))
                       (cons (file-name-directory byte-compile-dest-file)
                             load-path)
                     load-path)))
    (load "cc-mode" nil t)
    (load "cc-fonts" nil t)
    (load "cc-langs" nil t)
    (load "cc-bytecomp" nil t)))

И объявляем наш режим как наследник режима c-mode:

(eval-and-compile
  (c-add-language 'c2-mode 'c-mode))

На время пропустим последующий код и перейдём в его конец. Как обычно, основная часть кода режима заключается в следующих строках:

;;;###autoload
(define-derived-mode c2-mode prog-mode "C2"
  "Major mode for editing code written in the C2 Programming Language.

  Key bindings:
  \\{c2-mode-map}"
  :after-hook (c-update-modeline)
  (c-initialize-cc-mode t)
  (use-local-map c2-mode-map)
  (c-init-language-vars c2-mode)
  (c-common-init 'c2-mode)
  (c-run-mode-hooks 'c-mode-common-hook)
  (setq-local comment-start "// "
              comment-end ""
              block-comment-start "/*"
              block-comment-end "*/"))

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.\\(c2\\|c2i\\|c2t\\)\\'" . c2-mode))

(provide 'c2-mode)

Здесь в define-derived-mode создаётся новый режим, именуемый в коде c2-mode, наследуемый от prog-mode, имеющий название C2, которое будет отображаться в mode line, и строка документации со включением описания привязок к клавишам, которое описывается в переменной c2-mode-map:

(defvar c2-mode-map () "Keymap for C2 mode buffers.")
(if c2-mode-map nil (setq c2-mode-map (c-make-inherited-keymap)))

Я не стал добавлять никаких дополнительных комбинацией клавиш, а просто наследую их вызовом c-make-inherited-keymap.

Вернёмся к define-derived-mode и продолжим разбирать код построчно.

:after-hook (c-update-modeline)

c-update-modeline будет вызвано после срабатывания всех hook режима. Как я понял из кода, эта недокументированная функция настраивает mode line для нашего режима.

Вызов (c-initialize-cc-mode t) по-сути осуществляет то самое наследование от cc-mode, базового режима для C-подобных языков. Эта функция инициализирует cc-mode для текущего буфера. Опциональный параметр указывает, что нам нужна только базовая инициализация, а остальное мы сделаем сами.

Вызов (use-local-map c2-mode-map) задаёт для нашего режима клавиатурные комбинации, описанные раннее в переменной c2-mode-map.

Макрос (c-init-language-vars c2-mode) инициализирует для нашего режима все необходимые переменные.

Вызов (c-common-init 'c2-mode) выполняет инициализацию дополнительных возможностей для нашего режима. Внутри себя вызывает c-basic-common-init, выполняющую базовую инициализацию режима.

Вызов (c-run-mode-hooks 'c-mode-common-hook) вызывает общий hook для cc-mode.

В коде ниже настраивается подсветка комментариев:

(setq-local comment-start "// "
            comment-end ""
            block-comment-start "/*"
            block-comment-end "*/")

Но, как и следующий код, это не дало должного эффекта. Я так и не смог заставить cc-mode обрабатывать комментарии как комментарии. Хотя подобный код присутствовал в референсных режимах.

(c-lang-defconst c-block-comment-starter c2 "/*")
(c-lang-defconst c-block-comment-ender c2 "*/")
(c-lang-defconst c-comment-start-regexp c2 "/[*+/]")
(c-lang-defconst c-block-comment-start-regexp c2 "/[*+]")
(c-lang-defconst c-line-comment-starter c2 "//")

В следующей строке, как обычно, происходит связывание режима с определёнными расширениями имён файлов:

;;;###autoload
(add-to-list 'auto-mode-alist '("\\.\\(c2\\|c2i\\|c2t\\)\\'" . c2-mode))

Наконец, экспортируем наш режим:

(provide 'c2-mode)

Декларация синтаксических элементов

Вернёмся к пропущенному коду.

(defvar c-syntactic-element)

c-syntactic-element хранит синтаксические элементы и как-то используется внутри cc-mode.

(declare-function c-populate-syntax-table "cc-langs.el" (table))

c-populate-syntax-table содержит код обработки синтаксиса C-подобных языков, в частности для комментариев (!). Спрашивается, а что ж оно тогда не работает-то?

Далее идёт серия использования макроса c-lang-defconst, с помощью которого декларируются ключевые слова языка и прочие синтаксические элементы, такие как операторы.

(c-lang-defconst c-identifier-ops c2 '((left-assoc ".")))

Оператор точка объявляется левоассоциативным.

Дальше идёт описание всех ключевых слов для правильной интерпретации и подсветки. Не буду подробно комментировать, здесь довольно понятно, хотя и не всегда очевидно.

(c-lang-defconst c-ref-list-kwds
  c2 '("import" "local" "module"))

(c-lang-defconst c-protection-kwds
  c2 '("public"))

(c-lang-defconst c-constant-kwds
  c2 '("false" "true" "nil"))

(c-lang-defconst c-primitive-type-kwds
  c2 '("bool" "char" "f32" "f64" "i8" "i16" "i32" "i64" "isize" "u8" "u16" "u32" "u64" "usize" "void"))

(c-lang-defconst c-decl-start-kwds
  c2 '("fn" "module" "type"))

(c-lang-defconst c-type-prefix-kwds
  c2 '("enum" "struct" "union"))

(c-lang-defconst c-class-decl-kwds
  c2 '("enum" "struct" "union"))

(c-lang-defconst c-typedef-decl-kwds
  c2 '("type"))

(c-lang-defconst c-brace-list-decl-kwds
  c2 '("enum"))

(c-lang-defconst c-modifier-kwds
  c2 '("const" "local" "volatile"))

(c-lang-defconst c-block-stmt-1-kwds
  c2 '("as" "case" "do" "else"))

(c-lang-defconst c-block-stmt-2-kwds
  c2 '("for" "if" "sswitch" "switch" "while"))

(c-lang-defconst c-simple-stmt-kwds
  c2 '("assert" "break" "continue" "default" "elemsof" "fallthrough" "return" "sizeof" "static_assert"))

(c-lang-defconst c-paren-type-kwds
  c2 '("cast"))

(c-lang-defconst c-decl-hangon-kwds
  c2 '("as" "local"))

(c-lang-defconst c-paren-nontype-kwds
  c2 '("assert" "elemsof" "sizeof" "static_assert"))

(c-lang-defconst c-paren-stmt-kwds
  c2 '("for" "if" "sswitch" "swtich" "while"))

(c-lang-defconst c-label-kwds
  c2 '("case" "default"))

(c-lang-defconst c-before-label-kwds
  c2 '("goto"))

(c-lang-defconst c-return-kwds
  c2 '("return"))

Перечисляем операторы присваивания:

(c-lang-defconst c-assignment-operators
  c2 '("=" "*=" "/=" "%=" "+=" "-=" "<<=" ">>=" "&=" "^=" "|="))

Описываем операторы (здесь тоже всё довольно понятно):

(c-lang-defconst c-operators
  c2 `(
       (left-assoc ".")
       (postfix "(" ")" "[" "]" "++" "--")
       (prefix "!" "-" "~" "*" "&" "++" "--")
       (infix "." "*" "/" "%" "<<" ">>" "^" "|" "&" "+" "-")
       (infix "==" "!=" ">=" "<=" ">" "<" "&&" "||")
       (ternary "?:")
       (infix "=" "*=" "/=" "%=" "+=" "-=" "<<=" ">>=" "&=" "^=" "|=")
       (infix ",")))

Описываем операторы-скобки и звёздочку:

(c-lang-defconst c-opt-type-suffix-key
  c2 (concat "\\(\\[" (c-lang-const c-simple-ws) "*\\]\\|\\*\\)"))

Описываем уровни подсветки, чтобы управлять скоростью и качеством:

(defconst c2-font-lock-keywords-1 (c-lang-const c-matchers-1 c2)
  "Minimal highlighting for C2 mode.")
(defconst c2-font-lock-keywords-2 (c-lang-const c-matchers-2 c2)
  "Fast normal highlighting for C2 mode.")
(defconst c2-font-lock-keywords-3 (c-lang-const c-matchers-3 c2)
  "Accurate normal highlighting for C2 mode.")
(defvar c2-font-lock-keywords c2-font-lock-keywords-3
  "Default expressions to highlight in C2 mode.")

Фух! Код режима закончился. Работоспособность режима можно оценить по обложке к статье.

Заключение

Лёгкой прогулки не вышло. Как обычно сложность кода перенесли из одного места в другое. Не знаю, стоит ли использовать этот подход для очередного C-подобного языка или нет. Вот разработчики rust-mode написали весь код с нуля. А ещё есть SMIE и Tree Sitter. Так что не прощаемся, я думаю...

(c) Симоненко Евгений, 2025

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Нужен обзор языка C2?
28.57% Конечно!4
14.29% Лучше про C3 расскажи2
57.14% Про Emacs Lisp интересней почитать8
Проголосовали 14 пользователей. Воздержался 1 пользователь.
Теги:
Хабы:
+8
Комментарии11

Публикации

Ближайшие события