В 2019 году с февраля по март у меня был проект — Агидель. Обратил внимание на это, когда изучал свои старые репозитории на гитхабе, пока передвигал их на сурсхат в июле 2023. В старом блоге нашлась интересная статья про это. Переопубликовываю! С комментариями и дополнениями!
Я также переопубликовал исходный код под лицензией CC0. Ссылки в конце статьи.
Про Агидель
Языков программирования/разметки очень много, даже больше, чем сто. Разных синтаксисов чуть меньше. Так какой же синтаксис лучший? Я знаю какой. Этот синтаксис называется лиспоподобным синтаксисом. Его отличительная черта — много скобок. Вот примеры вычисления чисел фибоначчи на разных лиспах:
;; 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. С этого момента стал коммитить всё на гитхаб. Я старался делать хотя бы один коммит в день (и у меня получалось). Теперь у меня в профиле красивая зелёная полоска.
Забавно это читать в 2023, когда большая часть моих публичных коммитов на гитхаб — зеркала с сурсхата, репозитории Агидели с гитхаба удаляются, а эта статья перевыпускается с гитхаб-пейчжс на микоризу.
Вот тот же макрос, который я привёл в качестве примера прошлой итерации, только новой версии:
(-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, с ним, наверное, неплохо знакомы все. В следующей статье я расскажу, как использовать его не по назначению. Про это уже написали другие люди, кстати:
Заключение
В общем-то, Агидель можно использовать. Вот два репозитория:
Ещё нашёлся какой-то отдельный репозиторий с синтрансами. Это либо огрызок второй итерации, либо пригрызок недоделанной четвёртой итерации. Не стал разбираться.
Документации нет. Может быть, я её даже напишу. Может быть, я найду вдохновение и буду использовать Агидель не для генерации си, а для чего-нибудь ещё, и продолжку разработку. Но может и нет.
Документацию я не написал, вдохновение я так и не нашёл.