it-roy-ru.com

Когда имеет смысл сначала скомпилировать свой язык в код на C?

При разработке собственного языка программирования, когда имеет смысл писать конвертер, который берет исходный код и преобразует его в код на языке C или C++, чтобы я мог использовать существующий компилятор, такой как gcc, для получения машинного кода? Есть проекты, которые используют этот подход?

38
danijar

Перевод на C-код - очень хорошо сложившаяся привычка. Оригинальный C с классами (и ранние реализации C++, которые затем назывались Cfront ) сделали это успешно. Несколько реализаций LISP или Схемы делают это, например, Схема курицы , Scheme48 , Bigloo . Некоторые люди переводят Пролог на C . И некоторые версии Моцарт (и были попытки скомпилировать байт-код Ocaml в C ). Искусственный интеллект Дж. Питрата система CAIA также загружается и генерирует весь свой C-код. Vala также переводится в C для кода, связанного с GTK. В книге Квиннека LISP In Small Pieces есть глава о переводе на C.

Одной из проблем при переводе на C является хвостовые рекурсивные вызовы . Стандарт C не гарантирует, что компилятор C правильно их переводит (в "переход с аргументами", т. Е. без использования стека вызовов), даже если в некоторых случаях последние версии GCC (или Clang/LLVM) выполняют эту оптимизацию.

Другая проблема сборка мусора . Несколько реализаций просто используют консервативный сборщик мусора Boehm (который дружественный C ...). Если вы хотите собрать сборщик мусора (как это делают несколько реализаций LISP, например, SBCL), это может стать кошмаром (вы хотели бы dlclose в Posix).

Еще одна проблема связана с первоклассным продолжение и call/cc . Но возможны хитрые трюки (загляните внутрь Chicken Scheme). Доступ к стеку вызовов может потребовать много хитростей (но смотрите обратная трассировка GN и т. Д ....). Ортогональное постоянство продолжений (то есть стеков или потоков) было бы трудно в C.

Обработка исключений часто является вопросом для создания умных вызовов longjmp и т. Д ...

Возможно, вы захотите сгенерировать (в своем излучаемом коде C) соответствующие директивы #line. Это скучно и требует много работы (вы захотите, чтобы, например, было проще создавать gdb- отлаживаемый код).

Мой устаревший GCC MELT lispy специфичный для домена язык (для настройки или расширения GCC ) переведен на C (на самом деле сейчас это плохо для C++) , Он имеет свой собственный сборщик мусора для копирования. (Вас может заинтересовать Qish или Ravenbrook MPS ). На самом деле, генерация GC проще в машинно-сгенерированном C-коде, чем в рукописном C-коде (потому что вы настроите генератор C-кода для барьера записи и GC-оборудования).

Я не знаю какой-либо языковой реализации, переводящей подлинный код C++, т.е. использующий некоторую технику "сборки мусора во время компиляции", чтобы испускать код C++, используя много шаблонов STL и соблюдение RAII идиома. (скажите, пожалуйста, знаете ли вы один).

Что смешно сегодня, так это то, что (на современных настольных системах Linux) компиляторы C могут быть достаточно быстрыми для реализации интерактивного верхнего уровня read-eval-print-loop в переводе на C: вы будете испускать код C (a несколько сотен строк) при каждом взаимодействии с пользователем вы будете fork компилировать его в общий объект, который затем будет dlopen. (MELT делает все это готовым, и обычно это достаточно быстро). Все это может занять несколько десятых секунды и быть приемлемым для конечных пользователей.

Когда это возможно, я бы рекомендовал переводить на C, а не на C++, в частности, потому что компиляция C++ идет медленно. Однако сегодня C++ имеет мощный стандарт контейнеры , исключения , λ-выражения и т. Д .... и используется или требуется интересными библиотеками C++. или фреймворки, такие как Qt , POCO , Tensorflow , и все эти функции мотивируют выбор генерации кода C++ в моем любимом проекте под названием RefPerSys . Если генерируется C++ динамически, либо согласитесь подождать более секунды для компиляции каждого сгенерированного файла C++ (например, во временный плагин , смотрите для Linux --- C++ dlopen mini howto ) или используйте хитрые трюки (например, ccache и/или предварительно скомпилированные заголовки GCC и т. д ....), минимизируя, если это возможно, общую сумму #include - d материал), чтобы уменьшить время компиляции C++.

Если вы реализуете свой язык, вы могли бы также рассмотреть (вместо испускания кода C) некоторые JIT библиотеки вроде libjit , GNU молния , asmjit или даже LLVM или GCCJIT . Если вы хотите перевести на C, вы можете иногда использовать tinycc : он очень быстро компилирует сгенерированный код C (даже в памяти) на медленный машинный код. Но в целом вы хотите воспользоваться преимуществами оптимизации , которые выполняются реальным компилятором C, таким как GCC

Если вы переводите на свой язык C, убедитесь, что сначала вы собрали весь AST сгенерированного кода C в памяти (это также облегчает сначала генерировать все декларации, затем все определения и функциональный код). Вы могли бы сделать некоторые оптимизации/нормализации таким образом. Также вас могут заинтересовать несколько расширения GCC (например, вычисленные gotos). Вы, вероятно, захотите избежать генерации огромных функций C - например, из сотен тысяч строк сгенерированного C лучше разбить их на более мелкие части), так как оптимизирующие компиляторы C очень недовольны очень большими функциями C (на практике и экспериментально, gcc -O время компиляции больших функций пропорционально квадрату размера кода функции). Поэтому ограничьте размер сгенерированных C-функций до нескольких тысяч строк каждая.

Обратите внимание, что оба --- (Clang (thru LLVM ) и GCC = (thru libgccjit ) Компиляторы C & C++ предлагают некоторый способ испускания некоторых внутренних представлений, подходящих для этих компиляторов, но сделать это может (или нет) сложнее, чем испускать код C (или C++), и это специфичные для каждого компилятора.

Если вы разрабатываете язык для перевода на C, вы, вероятно, захотите использовать несколько приемов (или конструкций) для создания смеси C с вашим языком. Моя статья DSL2011 MELT: язык для конкретного домена, встроенный в компилятор GCC должна дать вам полезные советы.

55
Basile Starynkevitch

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

Обычно доменные языки написаны таким образом, система очень высокого уровня используется для определения или описания процесса, который затем компилируется в исполняемый файл или dll. Время, затрачиваемое на создание работающей/хорошей сборки, намного больше, чем на генерацию C, и C довольно близок к коду сборки по производительности, поэтому имеет смысл сгенерировать C и повторно использовать навыки разработчиков компиляторов C. Обратите внимание, что это не просто компиляция, но и оптимизация - парни, которые пишут gcc или llvm, потратили много времени на создание оптимизированного машинного кода, было бы глупо пытаться заново изобрести всю свою тяжелую работу.

Возможно, было бы более приемлемо повторно использовать бэкэнд компилятора LLVM, который IIRC не зависит от языка, поэтому вы генерируете инструкции LLVM вместо C-кода.

8
gbjbaanb

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

Например, предположим, что язык поддерживает функцию для выдачи UInt32 из четырех последовательных байтов произвольно выровненного UInt8[], интерпретируется в порядке байтов. На некоторых компиляторах можно написать код как:

uint32_t dat = *(__packed uint32_t*)p;
return (dat >> 24) | (dat >> 8) | ((uint32_t)dat << 8) | ((uint32_t)dat << 24));

и пусть компилятор сгенерирует операцию загрузки Word, за которой последует инструкция обратного байта в слове. Однако некоторые компиляторы не поддерживают модификатор __packed и при его отсутствии генерируют код, который не будет работать.

В качестве альтернативы можно написать код как:

return dat[3] | ((uint16_t)dat[2] << 8) | ((uint32_t)dat[1] << 16) | ((uint32_t)dat[0] << 24);

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

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

uint32_t result = 1u*x*y;

Без этого 1u, компилятор в системе, где INT_BITS варьировался от 33 до 64, мог законно делать все, что хотел, если произведение x и y было больше чем 2 147 483 647, и некоторые компиляторы склонны использовать такие возможности.

2
supercat

Вы получили несколько превосходных ответов выше, но, учитывая, что в комментарии вы ответили на вопрос "Почему вы хотите создать свой собственный язык программирования в первую очередь?" На "Это было бы в основном для целей обучения", "Я" Я собираюсь ответить под другим углом.

Имеет смысл написать конвертер, который берет исходный код и преобразует его в код на языке C или C++, так что вы можете использовать существующий компилятор, такой как gcc, чтобы в итоге получить машинный код, если вас больше интересует изучение лексического, синтаксического и семантический анализ, чем вы в изучении генерации кода и оптимизации!

Написание собственного генератора машинного кода - довольно значительная часть работы, которую вы можете избежать, компилируя в C-код, если это не то, что вас в первую очередь интересует!

Однако, если вы знакомы с программой сборки и увлечены проблемами оптимизации кода на самом низком уровне, то непременно создайте генератор кода для обучения!

1
Carson63000