Про Агидель

Статья переопубликована в моём цифровом саду с дополнениями. Читайте лучше там!

Языков программирования/разметки очень много, даже больше, чем сто. Разных синтаксисов столько же. Так какой же синтаксис лучший? Я знаю какой. Этот синтаксис называется лиспоподобный синтаксис. Его отличительная черта: много скобок. Вот примеры вычисления чисел фибоначчи на разных лиспах:

;; 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)))

В реализации поддержки си в этой итерации я преуспел больше всего, но потом я разочаровался в Агидели вообще.

Почему я разочаровался?

Я уже реализовал почти весь си. Я даже написал пару программ на Агидель/си, которые прекрасно транспилировались в валидный си. Так что не так? Я сравнил кусок кода на си, который я перевёл, и кусок кода на Агидель/си. Внимательно посмотрел. Смотрел. Смотрел. Понял следующее:

  1. Код на голом си читабельнее и понятнее.
  2. Когда я пишу на Агидель/си, я на самом деле пишу на голом си в голове и потом выражаю это на Агидель/си. То есть, выполняю двойную работу. К тому же,
  3. Мелкие косяки мешали брать и использовать Агидель/си везде и всюду. Что-то не реализовано, тут всё вылетает, потому что макросы в схеме дурацкие, а тут вообще семиколоны лишние получаются.

Взвесил всё на весах. Решил, что можно разочароваться.

Потери

Начались моральные страдания от того, что я несколько месяцев делал проект, в котором в итоге разочаровался. Ребята из чата «Клавиатуры и микроконтроллеры» не бросили меня, поддержали. Рассказали про важность полученного опыта. Оказалось, что даже кто-то ждал релиза первой версии, чтобы пользоваться Агиделью. Получил немного вдохновения для четвёртой итерации, но быстро его потерял.

Но пока я делал Агидель, я безнадёжно влюбился в макро-процессоры. В мои планы входит изучить что там на рынке есть. Я неплохо знаком с C Preprocessor, с ним, наверное, неплохо знакомы все. В следующей статье я расскажу, как использовать его не по назначению. Про это уже написано, кстати:

Заключение

В общем-то, Агидель можно использовать. Вот два репозитория:

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