Создание пригодных для использования языков JVM: обзор

Опубликовано: 2022-03-11

Есть несколько возможных причин для создания языка, некоторые из которых не сразу очевидны. Я хотел бы представить их вместе с подходом к созданию языка для виртуальной машины Java (JVM), максимально используя существующие инструменты. Таким образом, мы уменьшим усилия по разработке и предоставим набор инструментов, знакомый пользователю, что упростит внедрение нашего нового языка программирования.

Создание пригодных для использования языков JVM: обзор

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

Зачем создавать свой язык JVM?

Существует уже бесконечное количество языков программирования. Так зачем создавать новый? На это есть много возможных ответов.

Во-первых, существует множество разных языков: вы хотите создать язык программирования общего назначения (GPL) или язык для предметной области? К первому типу относятся такие языки, как Java или Scala: языки, предназначенные для написания достаточно приличных решений для большого набора проблем. Вместо этого предметно-ориентированные языки (DSL) сосредотачиваются на очень хорошем решении определенного набора проблем. Подумайте о HTML или Latex: вы можете рисовать на экране или генерировать документы на Java, но это будет громоздко, вместо этого с этими DSL вы можете очень легко создавать документы, но они ограничены этой конкретной областью.

Так что, возможно, есть набор проблем, над которыми вы работаете очень часто и для которых имеет смысл создать DSL. Язык, который сделает вас очень продуктивным при решении одних и тех же задач снова и снова.

Возможно, вместо этого вы захотите создать GPL, потому что у вас есть новые идеи, например, представить отношения как первоклассные граждане или представить контекст.

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

Дело в том, что если вы ориентируетесь на JVM, вы можете получить удобный язык с меньшими усилиями, потому что:

  • Вам просто нужно сгенерировать байт-код, и ваш код будет доступен на всех платформах, где есть JVM.
  • Вы сможете использовать все библиотеки и фреймворки, существующие для JVM.

Таким образом, стоимость разработки языка значительно снижается на JVM, и может иметь смысл создавать новые языки в сценариях, которые были бы неэкономичны вне JVM.

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

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

В идеале вы хотите иметь возможность:

  • Управление ссылками на код, скомпилированный для JVM, из других языков.
  • Редактируйте исходные файлы в своей любимой среде IDE с подсветкой синтаксиса, идентификацией ошибок и автозаполнением.
  • Вы хотите иметь возможность компилировать файлы, используя вашу любимую систему сборки: maven, gradle или другие.
  • Вы хотите иметь возможность писать тесты и запускать их как часть решения Continuous-Integration.

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

Итак, как мы можем этого достичь? В оставшейся части поста мы рассмотрим различные части, необходимые для того, чтобы сделать это возможным.

Разбор и компиляция

Первое, что вам нужно сделать, чтобы преобразовать исходные файлы в программу, — это проанализировать их, получив представление информации, содержащейся в коде, в виде абстрактного синтаксического дерева (AST). В этот момент вам нужно будет проверить код: есть ли синтаксические ошибки? Семантические ошибки? Вам нужно найти их все и сообщить о них пользователю. Если все идет гладко, вам все равно нужно разрешить символы. Например, относится ли «Список» к java.util.List или java.awt.List ? Когда вы вызываете перегруженный метод, какой из них вы вызываете? Наконец, вам нужно сгенерировать байт-код для вашей программы.

Итак, от исходного кода до скомпилированного байткода есть три основных этапа:

  1. Построение AST
  2. Анализ и преобразование AST
  3. Создание байт-кода из AST

Давайте посмотрим на эти фазы в деталях.

Построение AST : синтаксический анализ — это своего рода решенная проблема. Существует множество фреймворков, но я предлагаю вам использовать ANTLR. Он хорошо известен, хорошо поддерживается и имеет некоторые особенности, облегчающие определение грамматики (он обрабатывает менее рекурсивные правила — вам не нужно это понимать, но будьте благодарны, что он это делает!).

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

Создание байт-кода из AST : последний этап на самом деле не так уж и сложен. Вы должны были разрешить символы на предыдущем этапе и подготовить ландшафт, чтобы в основном вы могли преобразовать отдельные узлы вашего преобразованного AST в одну или несколько инструкций байт-кода. Структуры управления могут потребовать дополнительной работы, потому что вы собираетесь переводить свои циклы for, переключатели, ifs и т. д. в последовательность условных и безусловных переходов (да, ниже вашего красивого языка все еще будет куча переходов). Вам нужно узнать, как работает JVM внутри, но фактическая реализация не так сложна.

Интеграция с другими языками

Когда вы добьетесь мирового господства для своего языка, весь код будет писаться исключительно на нем. Однако в качестве промежуточного шага ваш язык, вероятно, будет использоваться вместе с другими языками JVM. Возможно, кто-то начнет писать пару классов или небольшие модули на вашем языке внутри более крупного проекта. Разумно ожидать, что вы сможете смешивать несколько языков JVM. Итак, как это влияет на ваши языковые инструменты?

Вам необходимо рассмотреть два различных сценария:

  • Ваш язык и другие живут в модулях, скомпилированных отдельно
  • Ваш язык и другие живут в одних и тех же модулях и компилируются вместе

В первом сценарии ваш код должен использовать только скомпилированный код, написанный на других языках. Например, некоторые зависимости, такие как Guava или модули в одном проекте, могут быть скомпилированы отдельно. Для такого рода интеграции требуются две вещи: во-первых, вы должны иметь возможность интерпретировать файлы классов, созданные другими языками, для преобразования в них символов и генерировать байт-код для вызова этих классов. Второй момент отражает первый: другие модули могут захотеть повторно использовать код, написанный на вашем языке, после того, как он был скомпилирован. Обычно это не проблема, потому что Java может взаимодействовать с большинством файлов классов. Однако вы все равно можете написать файлы классов, которые действительны для JVM, но не могут быть вызваны из Java (например, потому что вы используете идентификаторы, которые недействительны в Java).

Второй сценарий более сложен: предположим, у вас есть класс A, определенный в коде Java, и класс B, написанный на вашем языке. Предположим, что два класса ссылаются друг на друга (например, A может расширять B, а B может принимать A в качестве параметра для одного и того же метода). Теперь дело в том, что компилятор Java не может обработать код на вашем языке, поэтому вы должны предоставить ему файл класса для класса B. Однако для компиляции класса B вам нужно вставить ссылки на класс A. Итак, что вам нужно сделать, это чтобы иметь своего рода частичный компилятор Java, который, учитывая исходный файл Java, может интерпретировать его и создавать его модель, которую вы можете использовать для компиляции вашего класса B. Обратите внимание, что это требует, чтобы вы могли анализировать код Java (используя что-то вроде JavaParser) и решать символы. Если вы не знаете, с чего начать, взгляните на java-symbol-solver.

Инструменты: Gradle, Maven, Test Frameworks, CI

Хорошая новость заключается в том, что вы можете сделать тот факт, что они используют модуль, написанный на вашем языке, полностью прозрачным для пользователя, разработав плагин для gradle или maven. Вы можете указать системе сборки компилировать файлы на вашем языке программирования. Пользователь будет продолжать запускать mvn compile или gradle assemble и не заметит никакой разницы.

Плохая новость в том, что писать Maven-плагины непросто: документация очень скудная, не внятная и в основном устаревшая или просто неправильная . Да, звучит не утешительно. Я еще не писал плагины Gradle, но это кажется намного проще.

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

Мой совет — посмотреть доступные примеры: один из них — плагин Maven для языка программирования Turin.

После того, как вы его реализовали, каждый сможет легко компилировать исходные файлы, написанные на вашем языке, и использовать их в службах непрерывной интеграции, таких как Travis.

Плагин IDE

Плагин для IDE будет наиболее заметным инструментом для ваших пользователей и тем, что сильно повлияет на восприятие вашего языка. Хороший плагин может помочь пользователю выучить язык, предоставляя интеллектуальное автозаполнение, контекстные ошибки и предлагаемые рефакторинги.

Теперь наиболее распространенная стратегия — выбрать одну IDE (обычно Eclipse или IntelliJ IDEA) и разработать для нее специальный плагин. Это, вероятно, самая сложная часть вашей цепочки инструментов. Это происходит по нескольким причинам: во-первых, вы не можете разумно повторно использовать работу, которую потратите на разработку плагина для одной IDE, для других. Ваш Eclipse и ваш плагин IntelliJ будут полностью отдельными. Второй момент заключается в том, что разработка плагинов для IDE не очень распространена, поэтому документации не так много, а сообщество небольшое. Это означает, что вам придется потратить много времени на то, чтобы разобраться во всем самостоятельно. Я лично разрабатывал плагины для Eclipse и для IntelliJ IDEA. Мои вопросы на форумах Eclipse оставались без ответа в течение нескольких месяцев или лет. На форумах IntelliJ мне везло больше, и иногда я получал ответ от разработчиков. Однако пользовательская база разработчиков плагинов меньше, а API очень византийский. Приготовьтесь страдать.

Есть альтернатива всему этому, и это использование Xtext. Xtext — это платформа для разработки плагинов для Eclipse, IntelliJ IDEA и Интернета. Он родился на Eclipse и совсем недавно был расширен для поддержки других платформ, поэтому опыта в этом не так много, но это может быть альтернативой, достойной рассмотрения. Позвольте мне сказать прямо: единственный способ разработать очень хороший плагин — это разработать его с использованием собственного API каждой IDE. Однако с Xtext вы можете получить что-то достаточно приличное с небольшими усилиями - вы просто отдаете это синтаксису вашего языка, и вы получаете синтаксические ошибки/завершение бесплатно. Тем не менее, вы должны реализовать разрешение символов и сложные части, но это очень интересная отправная точка; однако трудными моментами являются интеграция со специфическими для платформы библиотеками для решения символов Java, поэтому на самом деле это не решит всех ваших проблем.

Выводы

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

В идеальном сценарии ваш пользователь мог бы клонировать простой проект, написанный на вашем языке, и собрать его с помощью стандартных инструментов (Maven или Gradle), не заметив никакой разницы. Если он хочет отредактировать проект, он может открыть его в своем любимом редакторе, и плагин поможет указать ему на ошибки и обеспечить умные дополнения. Это сценарий, сильно отличающийся от необходимости выяснять, как вызывать компилятор и редактировать файлы с помощью блокнота. Экосистема вокруг вашего языка действительно может изменить ситуацию, и в настоящее время ее можно построить, приложив разумные усилия.

Мой совет: проявляйте творческий подход к своему языку, но не к своим инструментам. Уменьшите первоначальные трудности, с которыми люди должны столкнуться, чтобы принять ваш язык, используя знакомые стандарты.

Удачного языкового проектирования!


Дальнейшее чтение в блоге Toptal Engineering:

  • Как подойти к написанию интерпретатора с нуля