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

Создаем свой собственный язык программирования с использованием LLVM. Часть 1: Лексический и синтаксический анализ

Время на прочтение36 мин
Количество просмотров25K
Всего голосов 39: ↑37 и ↓2+48
Комментарии16

Комментарии 16

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

Диалект Си, который заодно будет HDL (Hardware Description Language), но при этом будет оставаться настоящим Си (да, что-то из этой оперы уже есть — но количество макросов там такое навёрнуто, что Си там в принципе уже не определяется).

Ну, или можно сказать, что «это просто Си», но ограниченный дополнительными правилами так, что из него можно 1:1 собрать конфигурацию ПЛИС без «умных автоматических генераторов».

Например:

u12 Counter; //ладно, ОК, в плане типов это не совсем чистый Си :) Но только капельку!
void CntTick ()
  {
  if (Counter != 3999)
    Counter++;
  else Counter = 0;
  }


void main ()
  {
  while (true) //Main system clock
    {
      CntTick();
    }
  }

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

Counter++;
if (Counter == 4000) Counter = 0;

А вот с точки зрения HDL это сразу даёт синтаксическую ошибку — невозможно на одном такте произвести несколько последовательных операций с регистром. Или вот ещё:

Counter++;
Output = Counter; //Ошибка! Содержимое Counter будет доступно на шинах только на следующем такте.

Output = Counter = Counter+1; //А вот это нормально: просто вывели выхлоп сумматора на оба сразу и там защёлкнули.

То есть если немного покопать эту идею, можно и для нескольких Clock Domain придумать описание, и для чёрта лысого. И оно будет по-прежнему валидным Си, но при этом прямолинейно собираться в конфигу ПЛИС (это не отменяет необходимость «мыслить схемотехнически», конечно. Если «мыслить кодом» — получатся те самые синтаксические ошибки, на которые компилятор должен сразу поругаться, потому что смысла нет писать «для проверки» код, который потом не собирается в финальную конфигу ПЛИСа).

И всё равно спасибо за статью :) даже пусть и не соберусь никогда (разве что растормошат вопросами «а как на этом языке предполагается описать то-то и то-то?»).

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

Да, да, конечно, бэкенды тут совершенно разные нужны. К статье отношение имеет та половинка этого языка, которая собирается в test bench в виде программного кода. Вторая половинка (сбор того же самого сырца в конфигу ПЛИС) — это вообще нечто из совершенно другой плоскости, требующее «делать с нуля», совершенно согласен.

А фишка тут именно в том, что это не «обычный» язык высокого уровня, а хитро обстриженный до хардварного языка, то есть дающий полный контроль над процессом, на уровне VDHL.

u12 Counter = 0; //Не скомпилируется. Нельзя присвоить регистр «по включению питания».

u12 Counter;
flag PowerOnReset = 1; //А вот такие механизмы в ПЛИС есть.

void CntTick ()
  {
  if (Counter != 3999 && !PowerOnReset)
    Counter++;
  else Counter = 0;
  }
// Осталось где-то в другом процессе сбросить PowerOnReset по нажатию кнопки «Старт»

UPD: ну и с таким же успехом можно написать компилятор, собирающий VHDL в «программный код, в точности эмулирующий поведение ПЛИС». Это уже не на 50%, а на 100% по теме статьи :) Это тоже позволит сразу писать два решения — «стендовое софтовое» и «окончательное аппаратное», просто не на «обстриженном Си», а сразу на VHDL. Но, правда, «стендовое софтовое» не будет так весело интегрироваться куда угодно, потому что будет привязано к этому компилятору (а вот сишный вариант односторонне совместим с чем угодно).

На самом деле, в LLVM уже есть подпроект, который умеет генерить описание для ПЛИС. Он использует инфраструктуру для создания IR, которая называется MLIR (появилась в 2019 году). Кстати, MLIR (multi-level IR) и дизайн обычных языков упрощает ещё больше, чем голый LLVM. И да, это официальная часть LLVM. Название и ссылки на ПЛИС инструменты найду и скину завтра, когда буду у компа. Может интересно будет глянуть.

P.S. Там даже динамические типы и атрибуты можно с сентября 2022, планирую попробовать.

Такие ошибки проще обрабатывать на уровне семантичекого анализа, при проверке типов и корректности написанной программы.

Семантический — это будет часть 2 после синтаксического? :)

Кстати, в принципе, можно вообще обойтись одной проверкой корректности описания на стадии, собственно, синтеза конфиги ПЛИС по этому описанию, а потом то же самое отдать любому компилятору, уж он-то Си соберёт точно (тогда получится, что мы создали свой язык HDL, но язык программирования не создавали вовсе).

Но тогда мы теряем офигенно вкусную возможность прицепиться к той особенности именно «обкромсанного» Си, что в нём куча эмулируемых процессов работают одновременно, повлиять друг на друга в пределах такта не могут, а выходы «защёлкиваются» по тактированию. То есть все эти простынки всяких CntTick() можно автоматически раскидать по ядрам проца. В силу особенности ограничений, необходимых для того, чтобы язык был «на полставки HDL'ом», автоматическое разбиение на треды, балансировка и синхронизация их тоже превращаются в тривиальную задачу — в качестве побочного, но очень приятного эффекта.

Да, семантика будет разобрана во второй части.

Кстати, можно ведь сделать лексер+парсер (можно даже сгенерировать их что бы не тратить на них время) для генерации простого для работы представления, а потом реализовать семеантический анализ над этим представлением, после чего уже генерировать код для компилятора C (с добавлением дополнительной логики для параллелизма, и любых нужных манипуляций). Например, первый компилятор C++ - Cfront делал именно это, разбирал текст написанный на C++ и потом преобразовывал его в аналог написанный на C и потом уже полученный код компилировался обычным компилятором C. При таком подходе можно добавить все необходимые проверки на корректность программы, а так же добавить необходимую логику (например вызов каких-либо низкоуровневых конструкций)

У меня в полночь нафармились две «гульки» — одну отдаю в статью, а вторую — в карму :)

namespace tok {
enum TokenKind: unsigned short {
Тут напрашивается enum class, будет более тщательная проверка компилятором передаваемых значений.

Довольно странно, что не набежали адепты lexx/yacc/bison/antlr с возражениями, что лучше написать грамматику в БНФ, а парсеры сгенерит библиотека. Но я сам предпочитаю писать парсеры вручную, так можно получить больше контроля над всякими синтаксическими тонкостями.

Есть разные парсеры - LL и LR, в ручную в основном пишут LL, yacc и сотоварищи в основном генерят LR. Потенциально LR более скоростные и сильные, но там возникают проблемы, что если грамматика не однозначна задана, то появляются не разрешимые проблемы, так же с обработкой ошибок все бывает ни так гладко. К примеру, большинство парсеров C++ (gcc/llvm) написаны в ручную, как нисходящие рекурсивные LL.

  1. Да, можно использовать и `enum class`, но тут больше традиция использовании их в различных компиляторах, т.к. иногда для оптимизации в одно поле еще добавляют информацию о дополнительных флагах или еще чего-нибудь;

  2. Я привык писать рукописные парсеры и лексеры, тем более их написание обычно не самая сложная часть при создании компилятора/интерпретатора. Так же многие серьезные компиляторы тоже используют рукописные парсеры (C++ (clang), D и некоторые другие (например Roslyn)), т.к. при таком подходе можно специальным образом обрабатывать различные неоднозначности языка и не нужно приводить грамматику языка к определенному типу.

1. Да, понятно. Если например делать какой-то компилятор в байт-код и в опкоде бинарной операции например нижние 8 бит отдать под токен операции, то преобразование токена к unsigned будет удобнее записывать без лишних кастов.

А не проще было бы переложить задачу лексеринга и парсеринга на что-то вроде ANTLR? Чтобы просто написать грамматику, а код лексера и парсера генерировался сам.

Выше я дал ответ на этот вопрос.

Статьи однозначно нужные, доведите цикл до конца.

Но над стилем изложения следует поработать: сократить объём текста, сохранив его информативность (добрую половину текста в статье можно сократить, сохранив всё ценное от туда). Вот хорошая рекомендация по написанию статей.

https://www.sfu-prof.com/news/314-5-pravil-napisaniya-nauchnoj-stati

Спасибо за ссылку.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории