Все описанное в посте происходило несколько месяцев назад, и у меня долго не доходили лапки оформить пост, но все же…
Как вы уже, наверное, догадались из заголовка, речь пойдет про язык программирования Nim.
Я уже писал ранее про свои коммиты в Nim, но с тех пор пришлось еще немного в него поконтрибутить. На этот раз не в стандартную библиотеку, а аж в сам компилятор!
В современных языках обычно принято писать компиляторы на них же самих. Nim здесь не исключение. Когда-то давным-давно его компилятор был написан на Object Pascal, но с тех пор весь код на паскале автоматически сконвертили в код на Nim. Результат этой конвертации хорошо описывается словами из compiler/readme.md:
So the code is not a poster child of good Nim code.
Другая особенность компилятора Nim — это то, что он не использует LLVM (хотя существует сторонняя реализация, которая юзает LLVM как бэкенд компиляции), а транслирует код в Си, и только потом уже собирает бинарник при помощи
gcc, clang или любого другого компилятора, который существует в системе. Подход хорош тем, что позволяет очень и очень просто заюзать любую функцию из любой библиотеки на Си (просто указываешь, какую функцию из какого хедера ты хочешь импортировать в Nim, и какую библиотеку надо влинковать — и все работает. А этот ваш Rust так может?). Еще можно попросить компилятор скомпилить код не в Си, а C++ — это может пригодиться для интеропа с C++ и позволяет юзать классы из кода на C++ в коде на Nim.Конечно же, у такой двухэтапной компиляции есть и проблемы. Основная заключается в том, что, вообще говоря, Си и C++ — языки с достаточно большим количеством странностей и не всегда консистентным поведением. Например, ссылки в C++, которые умеют неявно разыменовываться, а сами ссылки нельзя никак изменять. Или массивы фиксированного размера в Си, которые ведут себя иногда как значение, а иногда как указатель на первый элемент — зависит от того, где и как массив используется. И, конечно же, при переводе кода на C/C++ эти все особенности надо учитывать. Естественно, учет особенностей приводит к костылям и багам в компиляторе. А избегать их иногда сложно, потому что хочется хороший интероп с C/C++.
Я несколько раз находил баги компилятора (раз, два, три) разной степени критичности. Кстати, баги в Nim исправляют довольно оперативно — через неделю-две созданный мной issue уже оказывался исправленным и закрытым. Но вот один раз мой баг игнорили аж целую неделю, поэтому я решил попробовать свои силы и исправить его самостоятельно.
Десяток часов времени, горстка отладочных
echo по коду — и баг исправлен. Пора делать PR. Кстати, довольно много времени ушло не на то, чтобы понять причину бага, а на то, чтобы понять, как наиболее правильно его исправить и не поломать старые костыли, учитывающие всевозможные хитрые случаи. А еще в случае с C++ для фикса надо было уметь копировать ссылку — это сложная задача, поэтому пришлось немного покостылить и сделать так, чтобы вместо нее копировался указатель.В итоге баг повержен, PR залит, issue закрыт. Победа? А вот и нет!
Для начала пришлось немного попотеть, чтобы бэкпортировать фикс в stable ветку. При попытке это сделать падали тесты, и пришлось черри-пикнуть в stable еще один коммит, чтобы все заработало.
Во-вторых, оказалось, что я все-таки не учел все случаи, и мой фикс сломал чей-то код. Пришлось чинить. Впрочем, об этом я уже даже когда-то писал :)
Выводы из всей этой истории делайте сами. Могу лишь сказать вот что: если вам нравится язык, смотреть исходники его компилятора может быть не лучшей идеей — можно разочароваться :) Тем не менее, даже несмотря на такой опыт с багами, я все еще считаю, что Nim как язык неплох.
Стоит отметить, что описанные выше проблемы с компиляцией пытаются потихоньку решить. В частности, авторы языка изобрели промежуточный байткод между самой логикой Nim и C/C++ бэкендами, что позволит, я надеюсь, упростить компилятор и уменьшить количество багов.