Переход на 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, должен дать вам полезные советы.