Про Агидель
Статья переопубликована в моём цифровом саду с дополнениями. Читайте лучше там!
Языков программирования/разметки очень много, даже больше, чем сто. Разных синтаксисов столько же. Так какой же синтаксис лучший? Я знаю какой. Этот синтаксис называется лиспоподобный синтаксис. Его отличительная черта: много скобок. Вот примеры вычисления чисел фибоначчи на разных лиспах:
;; Clojure
(defn fib [n]
(case n
0 0
1 1
(+ (fib (- n 1))
(fib (- n 2)))))
;; Common Lisp
(defun fibonacci-iterative (n &aux (f0 0) (f1 1))
(case n
(0 f0)
(1 f1)
(t (loop for n from 2 to n
for a = f0 then b and b = f1 then result
for result = (+ a b)
finally (return result)))))
;; Scheme
(define (fib-rec n)
(if (< n 2)
n
(+ (fib-rec (- n 1))
(fib-rec (- n 2)))))
Ну, в общем, понятно. Скобки. Я люблю скобки.
Я хотел иметь возможность писать на си, используя такие скобки. Что же делать? Сделать такую программу, которая на вход получает программу в лисповом синтаксисе:
(import stdio.h)
(defun (main int) ()
[printf "hello world!\n"] ; квадратные скобки для вызова сишных функций
(return 0))
И выводит ту же программу, но уже в сишном синтаксисе:
#include <stdio.h>
int main () {
printf("hello world!\n");
return 0;
}
Название такой программы пришло в голову быстро — Агидель, в честь реки. Со временем пришло осознание, что можно предоставить возможность пользователю добавлять поддержку других языков.
То есть, я планировал сделать макро-процессор с синтаксисом как в лиспе.
Первая итерация
Сразу начал делать. Придумал архитектуру, которая оставалась в остальных итерациях: синтрансы и плагины.
Синтрансы (syntrans = syntax transformer) — штуки, которые получают на вход исходный код в одной форме и выводят его же в другой форме, что-нибудь изменив. Например, один синтранс вырезает комментарии из кода, другой преобразует мои расширения лиспового синтаксиса в обычные s-выражения, другой всё в итоге превращает во что-то исполняемое.
Плагины — просто наборы макросов. Пользователь сам подключает те, которые ему нужны.
В первой итерации я имплементировал синтрансы как отдельные программы. Вот как-то так выполнялась транспиляция программ:
$ cat hello-world.lisp | agidel-discomment | agidel-disbracket\
| agidel-quotify | agidel-prepare | bash - > hello-world.c
То есть, в итоге текст на Агидели превращался в скрипт на баше. Этот скрипт вызывал макросы, которые тоже были реализованы как отдельные мини-программы. Довольно элегантно. Такая архитектура позволяла писать отдельные макросы на любом языке. Вот один из них:
#include <stdio.h>
#include <stdbool.h>
#include "libagidel.h"
int main(int argc, char** argv) {
for (int i = 1; i < argc; i++) {
if (is_string(argv[i]) || is_angled(argv[i]))
printf("#include %s\n", argv[i]);
else
printf("#include <%s>\n", argv[i]);
}
return 0;
}
Я сидел-реализовывал поддержку си, но потом я решил, что как-то мерзенько вышло. Поменял архитектуру и стал делать с нуля.
Вторая итерация
Решил и синтрансы, и плагины реализовать как модули схемы. Схема — диалект лиспа, самый классный из них. У схемы очень много реализаций, я выбрал ту, которая называется Chicken Scheme. С этого момента стал коммитить всё на гитхаб. Я старался делать хотя бы один коммит в день (и у меня получалось). Теперь у меня в профиле красивая зелёная полоска.
Вот тот же макрос, который я привёл в качестве примера прошлой итерации, только новой версии:
(-define-syntax
import
(syntax-rules ()
((_ o ...) (-string-append
(-map
(-lambda (f)
(-if (-string? f)
(format "#include ~A\n" f)
(format "#include <~A>\n" f))))))))
Как видно, почти все схемовские функции начинаются с дефиса. Я сделал это, чтобы избежать коллизии имён с агидельными макросами.
Как и в прошлый раз, ядро сделал, начал реализовывать поддержку си, но потом решил, что опять вышло как-то по-дурацки. Поменял архитектуру.
Третья итерация
Это был микс первых двух итераций. Синтрансы реализованы как отдельные мини-программы, а плагины как модули схемы, которые подрубались когда надо. Главной программой являлся простой скрипт на баше, который генерировал код на схеме, который скармливал интерпретатору прямо там. Потом я переписал эту часть на Агидель/sh. Вот так выглядит исходный код главной программы:
(shebang!)
(set import_statement
"(import (prefix (only scheme define string-append display)
AGIDEL/)
(only scheme quote)")
(for-each-cli-arg
plugin
(set import_statement + "(agidel-plugin $plugin)"))
(set import_statement + ")")
[csi -batch -quiet -eval
"(begin
(module agidel_temp (main)
$import_statement
(AGIDEL/define (main)
(AGIDEL/display (AGIDEL/string-append $(cat /dev/stdin)))))
(import agidel_temp)
(main))"]
[echo]
Верно, я так далеко дошёл в этой итерации, что написал Агидель на Агидели.
Тот же макрос, что и в прошлых двух итерациях, но ещё раз:
(define (import . fs)
(-apply -string-append
(-map (lambda (f)
(-if (-string? f)
(format "#include \"~A\"\n" f)
(format "#include <~A>\n" f)))
fs)))
В реализации поддержки си в этой итерации я преуспел больше всего, но потом я разочаровался в Агидели вообще.
Почему я разочаровался?
Я уже реализовал почти весь си. Я даже написал пару программ на Агидель/си, которые прекрасно транспилировались в валидный си. Так что не так? Я сравнил кусок кода на си, который я перевёл, и кусок кода на Агидель/си. Внимательно посмотрел. Смотрел. Смотрел. Понял следующее:
- Код на голом си читабельнее и понятнее.
- Когда я пишу на Агидель/си, я на самом деле пишу на голом си в голове и потом выражаю это на Агидель/си. То есть, выполняю двойную работу. К тому же,
- Мелкие косяки мешали брать и использовать Агидель/си везде и всюду. Что-то не реализовано, тут всё вылетает, потому что макросы в схеме дурацкие, а тут вообще семиколоны лишние получаются.
Взвесил всё на весах. Решил, что можно разочароваться.
Потери
Начались моральные страдания от того, что я несколько месяцев делал проект, в котором в итоге разочаровался. Ребята из чата «Клавиатуры и микроконтроллеры» не бросили меня, поддержали. Рассказали про важность полученного опыта. Оказалось, что даже кто-то ждал релиза первой версии, чтобы пользоваться Агиделью. Получил немного вдохновения для четвёртой итерации, но быстро его потерял.
Но пока я делал Агидель, я безнадёжно влюбился в макро-процессоры. В мои планы входит изучить что там на рынке есть. Я неплохо знаком с C Preprocessor, с ним, наверное, неплохо знакомы все. В следующей статье я расскажу, как использовать его не по назначению. Про это уже написано, кстати:
Заключение
В общем-то, Агидель можно использовать. Вот два репозитория:
Документации нет. Может быть, я её даже напишу. Может быть, я найду вдохновение и буду использовать Агидель не для генерации си, а для чего-нибудь ещё, и продолжку разработку. Но может и нет.