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

от автора

Недавно я разработал ещё один режим 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?

0% Конечно!0
0% Лучше про C3 расскажи0
0% Про Emacs Lisp интересней почитать0

Никто еще не голосовал. Воздержавшихся нет.

ссылка на оригинал статьи https://habr.com/ru/articles/911648/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *