Создано понедельник, 05 сентября 2022 г.

Эта запись в блоге посвящена урокам, которые я извлек из реализации ретро-диалекта Лиспа в стиле 80-х в Go, что я мог бы сделать лучше и на что следует обратить внимание. Lisp далеко не закончен, и нужно сделать много переносов из другого репозитория, но он пригоден для использования и достаточно зрел, чтобы немного подумать о том, как я его создал.

Первые ошибки и как их избежать

Диалект называется Z3S5 Lisp и основан на одностраничном лиспе под названием Nukata Lisp. Одна ошибка, которую я совершил, заключалась в том, что я сохранил базовую реализацию и начал ее расширять, внося изменения в базовый Лисп лишь изредка и по мере того, как я считал их подходящими. В то время это казалось хорошей идеей, поскольку Lisp планировалось использовать только в виртуальной машине Lisp из параллельной вселенной, которая должна была стать образовательной забавной игрушкой и не использоваться ни для чего другого, кроме хакерства. Что приводит к первому уроку:

Урок 1: Никогда не думайте, что ваш язык будет использоваться только по прямому назначению! Он будет использован для чего-то другого.

Реализуя свою игровую виртуальную машину на Лиспе, я был совершенно уверен, что никогда больше нигде не буду использовать этот игрушечный Лисп. Я был неправ. После оценки вариантов встроенного языка сценариев в другом, более серьезном проекте, я решил исключить Лисп, чтобы сделать его пригодным для использования в других проектах. Хотя есть много плюсов и минусов, и большинство пользователей разумно ожидают, что Common Lisp или диалект схемы будут использоваться в качестве встроенного языка сценариев, решение может быть вполне разумным. Мне нужен полный контроль над реализацией и очень тесная интеграция с Go — ни ECL, ни схемы вроде Guile для этого не очень хороши, так как их и так тяжело включать в программы на C, а тесная интеграция с Go была бы огромным начинанием. Существует много других языков сценариев для Go, таких как Tengo и интерпретируемая версия Go, но все они имеют свои недостатки: очень сложно проверить их последствия для безопасности, не понимая весь исходный код, и они создают очень большую зависимость. Что, если их создатели обратятся к чему-то другому, а репозиторий будет заброшен? В конце концов, использование моего собственного диалекта Лиспа было разумным выбором, хотя вполне вероятно, что я предложу дополнительный интерфейс Common Lisp, если программа окажется успешной.

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

Урок 2: Не используйте пустые интерфейсы!

Программисты Go знают, что interface{}, также известный как any, следует использовать с осторожностью, а с появлением универсальных шаблонов в Go 1.19+ это правило стало еще более логичным. Однако для простоты Nukata Lisp (как одностраничный Lisp) использует пустой интерфейс в качестве механизма упаковки для всех типов данных. Я знал, что это проблематично, но не менял. На данный момент изменить его уже не вариант. Однако в результате расширение системы с помощью пользовательских типов данных оказывается намного сложнее, чем должно быть. Если вы создаете свой собственный интерпретатор, сделайте себе одолжение и поместите значения в свой собственный тип интерфейса, который требует методов, которые нужны интерпретатору. Я предлагаю: печать, информацию о типе времени выполнения по мере необходимости, экстернализация и интернализация, равенство, хеш-значения, сигнатуры функций для функциональных типов данных и, возможно, даже финализаторы и инициализаторы. Убедитесь, что любой, кто хочет добавить тип данных, может сделать это, реализовав этот интерфейс.

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

Урок 3: Думайте об отладке с самого начала!

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

Урок 4: Сначала консолидируйте, а затем добавляйте дополнительные команды!

Полный энтузиазма по поводу моей новой машины Lisp, я добавил новые типы данных и множество новых команд на стороне Go. Хотя это сработало для меня хорошо из-за моих автоматически сгенерированных документов, это, как правило, не очень хорошая идея. Причина должна быть очевидной, в начале может иметь смысл внести большие изменения в дизайн. Небольшой размер основного языка позволяет вам рефакторить код без особых усилий, пока вы не будете уверены, что он достаточно стабилен. Только после этого следует добавлять дополнительные функции.

Урок 5: Думайте о документации с самого начала!

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

Зачем создавать свой собственный язык сценариев?

Есть несколько огромных преимуществ в создании собственного маленького языка расширений:

Полный контроль над всем

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

Скорость может быть ужасной, но для многих целей производительность совершенно не имеет значения. В моем случае я использую Z3S5 Lisp в качестве встроенного языка сценариев для системы плагинов, которая в основном просто подключается к функциям на стороне Go. Если что-то работает слишком медленно на стороне Lisp, всегда есть возможность предоставить для этого быструю специализированную функцию на стороне Go. Поэтому скорость не имеет большого значения, а удобство сопровождения и полный контроль над направлением и функциями языка сценариев более важны.

Простая интеграция

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

Лучшая безопасность

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

Насколько подходит Go для интерпретаторов?

В целом, я очень доволен использованием Go для этого интерпретатора. Это был ценный урок, и я многому научился у Nukata Lisp о том, как реализовать простую некомпилируемую систему Lisp. Если вам нужна максимальная производительность в интерпретаторе, то C, вероятно, по-прежнему лучший выбор. Есть также интересные инструменты, такие как Jiitter, своего рода оптимизирующий фреймворк интерпретатора, близкий к JIT-компилятору. Однако для умеренно быстрых интерпретаторов Go — отличный язык реализации. Он позволяет сопоставлять структуры Go с упакованными значениями времени выполнения и использовать преимущества подверженных ошибкам, но эффективных примитивов параллелизма Go. Предоставление горутин целевому языку мгновенно сделает его более интересным, чем многие обычные одноядерные оптимизированные интерпретаторы и виртуальные машины, поскольку их масштабирование на многоядерные процессоры довольно сложно. Я продолжу работать над Z3S5 Lisp для удовольствия — надеюсь, если позволит время, следующие большие улучшения сделают отладку намного проще!

[Эта история взята с сайта slothblog.org.]