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

35

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

Данияр
источник
4
Если вы посмотрите мимо C, вы увидите, что C # и Java также компилируются в промежуточные языки. Вы избавлены от необходимости переделывать большую работу, которую уже сделал кто-то другой, нацеливаясь на промежуточный язык вместо того, чтобы идти прямо к сборке.
Кейси
1
@emodendroket Тем не менее, C # и Java компилируются в IL, который предназначен для IL в целом и для C # / Java в частности, поэтому во многих отношениях байт-код CIL и JVM является более разумным и удобным, чем IL, чем C. Дело не в том, использовать ли какой-либо промежуточный язык, а в том, какой промежуточный язык использовать.
1
Посмотрите на несколько реализаций свободного программного обеспечения, генерирующих C-код. И я надеюсь, что вы сделаете вашу языковую реализацию свободным программным обеспечением.
Василий Старынкевич
2
Вот обновленная ссылка из комментария @ RobertHarvey: yosefk.com/blog/c-as-an-intermediate-language.html .
Кристиан Дин

Ответы:

52

Переход на C-код - очень хорошо сложившаяся привычка. Оригинальный C с классами (и ранние реализации C ++, тогда называемые Cfront ) сделали это успешно. Это делают несколько реализаций Lisp или Scheme, например, Chicken Scheme , Scheme48 , Bigloo . Некоторые люди перевели Пролог на Си . Как и некоторые версии Моцарта (и были попытки скомпилировать байт-код Ocaml в C ). Система CAIA искусственного интеллекта J.Pitrat также загружается и генерирует весь свой C-код. Vala также переводится как C для кода, связанного с GTK. Книга Квиннека Лисп в маленьких кусочках есть глава о переводе на C.

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

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

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

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

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

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

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

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

Когда это возможно, я бы рекомендовал переводить на C, а не на C ++, в частности, потому что компиляция C ++ идет медленно.

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

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

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

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

Василий Старынкевич
источник
Вы имеете в виду "Цыпленок Схема?"
Роберт Харви
1
Да. Я дал URL.
Василий Старынкевич
Относительно практично ли сделать виртуальную машину, такую ​​как Java или что-то еще, скомпилировать байт-код в C, а затем использовать gcc для JIT-компиляции? Или они должны просто перейти от байт-кода к сборке?
Panzercrisis
1
@Panzercrisis Большинству JIT-компиляторов требуются бэкэнды машинного кода для поддержки таких вещей, как замена функции и исправление существующего кода с помощью двери для перехода. Кроме того, gcc определенно ... архитектурно менее подходит для компиляции JIT и других вариантов использования. Проверьте libgccjit, хотя: gcc.gnu.org/ml/gcc-patches/2013-10/msg00228.html и gcc.gnu.org/wiki/JIT
1
Отличный материал для ориентации. Благодарность!
8

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

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

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

gbjbaanb
источник
Похоже, библиотеки - довольно веская причина, чтобы рассмотреть это тоже.
Кейси
Когда вы говорите «ваш« ИЛ »», на что вы ссылаетесь? Абстрактное синтаксическое дерево?
Роберт Харви
@RobertHarvey нет, я имею в виду C-код. В случае OP это промежуточный язык на полпути между его собственным языком высокого уровня и машинным кодом. Я поставил его в кавычки, чтобы попытаться выразить идею, что это не IL, как это используют многие люди (например, Microsoft .NET IL)
gbjbaanb
2

Написание компилятора для создания машинного кода может быть не намного сложнее, чем написание того, который производит 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));

и пусть компилятор сгенерирует операцию загрузки слова с последующей инструкцией обратного байта в слове. Однако некоторые компиляторы не поддерживают модификатор __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, и некоторые компиляторы склонны использовать такие возможности.

Supercat
источник
1

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

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

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

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

Carson63000
источник
-7

Это зависит от того, какую операционную систему вы используете, если вы используете Windows, существует Microsoft IL (промежуточный язык), который преобразует ваш код в промежуточный язык, так что вам не нужно время для компиляции в машинный код. Или, если вы используете Linux, для этого есть отдельный компилятор

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

Тайяб Гульшер Вохра
источник
2
Your code should be compiled into machine code to make it useful for machine- Если ваш компилятор выдает код c в качестве вывода, вы можете поместить код c в компилятор ac, чтобы получить машинный код, верно?
Роберт Харви,
да. потому что машина не говорит на языке c
Тайяб Гулшер Вохра
2
Правильно. Таким образом, вопрос заключался в том, «когда имеет смысл выпускать c и использовать компилятор ac, а не напрямую генерировать машинный язык или байт-код?»
Роберт Харви
на самом деле он просит спроектировать свой язык программирования, в котором он просит «преобразовать его в код C или C ++». Поэтому я объясняю это, если вы разрабатываете свой собственный язык программирования, почему вы должны использовать компилятор c или c ++. если вы достаточно умны, вы должны создать свой собственный
Tayyab Gulsher Vohra
8
Я не думаю, что вы понимаете вопрос. См. Yosefk.com/blog/c-as-an-intermediate-language.html
Роберт Харви,