¿Cuándo tiene sentido compilar primero mi propio lenguaje en código C?

35

Al diseñar un lenguaje de programación propio, ¿cuándo tiene sentido escribir un convertidor que tome el código fuente y lo convierta a código C o C ++ para que pueda usar un compilador existente como gcc para terminar con el código de máquina? ¿Hay proyectos que utilizan este enfoque?

danijar
fuente
44
Si mira más allá de C, verá que C # y Java también se compilan en lenguajes intermedios. Se ahorra tener que rehacer una gran cantidad de trabajo que otra persona ya ha hecho al apuntar a un idioma intermedio en lugar de ir directamente al ensamblado.
Casey
1
@emodendroket Sin embargo, C # y Java compilan a un IL que está diseñado para ser un IL en general y para C # / Java específicamente, por lo que en muchos sentidos los códigos de bytes CIL y JVM son más sensibles y convenientes como IL que C podría ser. No se trata de usar un idioma intermedio, se trata de qué idioma intermedio usar.
1
Mire varias implementaciones de software libre que generan código C. Y espero que haga que su implementación de lenguaje sea software libre.
Basile Starynkevitch
2
Aquí está el enlace actualizado del comentario de @ RobertHarvey : yosefk.com/blog/c-as-an-intermediate-language.html .
Christian Dean

Respuestas:

52

La traducción al código C es un hábito muy bien establecido. El C original con clases (y las primeras implementaciones de C ++, luego llamadas Cfront ) lo hicieron con éxito. Varias implementaciones de Lisp o Scheme lo están haciendo, por ejemplo, Chicken Scheme , Scheme48 , Bigloo . Algunas personas traducidos Prolog a C . Y también algunas versiones de Mozart (y ha habido intentos de compilar el código de bytes de Ocaml en C ). El sistema CAIA de inteligencia artificial de J.Pitrat también se inicia y genera todo su código C. Vala también se traduce a C, para el código relacionado con GTK. El libro de Queinnec Lisp In Small Pieces tener un capítulo sobre la traducción a C.

Uno de los problemas al traducir a C son las llamadas recursivas de cola . El estándar C no garantiza que un compilador de C los esté traduciendo correctamente (a un "salto con argumentos", es decir, sin comer pila de llamadas), incluso si en algunos casos, las versiones recientes de GCC (o de Clang / LLVM) hacen esa optimización .

Otro problema es la recolección de basura . Varias implementaciones solo usan el recolector de basura conservador Boehm (que es amigable con C ...). Si desea recolectar código basura (como lo hacen varias implementaciones de Lisp, por ejemplo, SBCL) eso podría ser una pesadilla (le gustaría dlcloseen Posix).

Otro problema más es tratar con las continuas de primera clase y call / cc . Pero son posibles trucos ingeniosos (mira dentro de Chicken Scheme). Acceder a la pila de llamadas podría requerir muchos trucos (pero vea GNU backtrace , etc.). La persistencia ortogonal de las continuaciones (es decir, de pilas o hilos) sería difícil en C.

El manejo de excepciones a menudo es un asunto para emitir llamadas inteligentes a longjmp, etc.

Es posible que desee generar (en su código C emitido) #linedirectivas apropiadas . Esto es aburrido y requiere mucho trabajo (querrás que, por ejemplo, produzca un gdbcódigo más fácil de depurar).

Mi lenguaje específico de dominio lispy MELT (para personalizar o ampliar GCC ) se traduce a C (en realidad a C ++ pobre ahora). Tiene su propio recolector de basura de copia generacional. (Quizás te interese Qish o Ravenbrook MPS ). En realidad, el GC generacional es más fácil en el código C generado por máquina que en el código C escrito a mano (porque personalizará su generador de código C para su barrera de escritura y maquinaria GC).

No conozco ninguna implementación de lenguaje que se traduzca a código C ++ genuino , es decir, que use alguna técnica de "recolección de basura en tiempo de compilación" para emitir código C ++ usando muchas plantillas STL y respetando el lenguaje RAII . (por favor diga si conoce uno).

Lo que es divertido hoy es que (en los escritorios actuales de Linux) los compiladores de C pueden ser lo suficientemente rápidos como para implementar un bucle de lectura-evaluación-impresión interactivo de nivel superior traducido a C: emitirá código C (unos cientos de líneas) a cada usuario interacción, lo forkcompilarás en un objeto compartido, lo que harías entonces dlopen. (MELT lo está haciendo todo listo, y generalmente es lo suficientemente rápido). Todo esto puede tomar algunas décimas de segundo y ser aceptado por los usuarios finales.

Cuando sea posible, recomendaría traducir a C, no a C ++, en particular porque la compilación de C ++ es lenta.

Si está implementando su lenguaje, también puede considerar (en lugar de emitir código C) algunas bibliotecas JIT como libjit , GNU lightning , asmjit o incluso LLVM o GCCJIT . Si desea traducir a C, a veces puede usar tinycc : compila muy rápidamente el código C generado (incluso en la memoria) para ralentizar el código de la máquina. Pero, en general, desea aprovechar las optimizaciones realizadas por un compilador de C real como GCC

Si traduce a C su lenguaje, asegúrese de construir primero todo el AST del código C generado en la memoria (esto también hace que sea más fácil generar primero todas las declaraciones, luego todas las definiciones y el código de función). Podrías hacer algunas optimizaciones / normalizaciones de esta manera. Además, podría estar interesado en varias extensiones de GCC (por ejemplo, gotos calculados). Probablemente querrá evitar generar funciones C enormes , por ejemplo, de una línea de cientos de miles de C generadas (será mejor que las divida en partes más pequeñas) ya que la optimización de los compiladores C es muy infeliz con funciones C muy grandes (en la práctica, y experimentalmente,gcc -OEl tiempo de compilación de las funciones grandes es proporcional al cuadrado del tamaño del código de la función). Por lo tanto, limite el tamaño de sus funciones C generadas a unos pocos miles de líneas cada una.

Tenga en cuenta que tanto los compiladores Clang (thru LLVM ) como GCC (thru libgccjit ) C & C ++ ofrecen alguna forma de emitir algunas representaciones internas adecuadas para estos compiladores, pero hacerlo podría (o no) ser más difícil que emitir código C (o C ++), y es específico para cada compilador.

Si diseña un lenguaje para traducirlo a C, probablemente desee tener varios trucos (o construcciones) para generar una mezcla de C con su lenguaje. Mi papel DSL2011 MELT: un lenguaje específico de dominio traducido incrustado en el compilador de GCC debería darle consejos útiles.

Basile Starynkevitch
fuente
¿Te refieres al "Esquema de pollo"?
Robert Harvey
1
Sí, le di la URL.
Basile Starynkevitch
¿Es relativamente práctico hacer una máquina virtual, como Java o algo así, compilar bytecode a C y luego usar gcc para la compilación JIT? ¿O deberían ir directamente del código de bytes al ensamblaje?
Panzercrisis
1
@Panzercrisis La mayoría de los compiladores JIT requieren sus backends de código de máquina para admitir cosas como reemplazar una función y parchear el código existente con una puerta de salto / trampa. Aparte de eso, gcc específicamente es ... arquitectónicamente menos adecuado para la compilación JIT y otros casos de uso. Echa un vistazo a libgccjit: gcc.gnu.org/ml/gcc-patches/2013-10/msg00228.html y gcc.gnu.org/wiki/JIT
1
Gran material de orientación. ¡Gracias!
8

Tiene sentido cuando el tiempo para generar el código completo de la máquina supera los inconvenientes de tener un paso intermedio de compilar su "IL" en el código de la máquina usando un compilador de C.

Por lo general, los lenguajes específicos de dominio se escriben de esta manera, se utiliza un sistema de muy alto nivel para definir o describir un proceso que luego se compila en un ejecutable o dll. El tiempo necesario para producir un ensamblaje funcional / bueno es mucho mayor que generar C, y C está bastante cerca del código de ensamblaje para el rendimiento, por lo que tiene sentido generar C y reutilizar las habilidades de los escritores del compilador de C. Tenga en cuenta que no solo se trata de compilar, sino también de optimizar: los chicos que escriben gcc o llvm han pasado mucho tiempo haciendo código de máquina optimizado, sería una tontería tratar de reinventar todo su arduo trabajo.

Puede ser más aceptable volver a usar el compilador de LLVM que IIRC es neutral en cuanto al lenguaje, por lo que genera instrucciones LLVM en lugar de código C.

gbjbaanb
fuente
Parece que las bibliotecas son una razón bastante convincente para considerarlo también.
Casey
Cuando dices "tu 'IL'", ¿a qué te refieres? ¿Un árbol de sintaxis abstracta?
Robert Harvey
@RobertHarvey no, me refiero al código C. En el caso de los OP, este es un lenguaje intermedio a medio camino entre su propio lenguaje de alto nivel y su código de máquina. Lo puse entre comillas para tratar de transmitir esta idea de que no es IL como lo usan muchas personas (es decir, .NET IL de Microsoft, por ejemplo)
gbjbaanb
2

Escribir un compilador para producir código de máquina puede no ser mucho más difícil que escribir uno que produzca C (en algunos casos puede ser más fácil), pero un compilador que produce código de máquina solo podrá producir programas ejecutables en la plataforma particular para la cual fue escrito; un compilador que produce código C, por el contrario, puede producir un programa para cualquier plataforma que use un dialecto de C que el código generado está diseñado para soportar. Tenga en cuenta que en muchos casos puede ser posible escribir código C que sea completamente portátil y que se comportará como se desee sin utilizar ningún comportamiento no garantizado por el estándar C, pero el código que se basa en comportamientos garantizados por la plataforma puede ejecutarse mucho más rápido en plataformas que hacen esas garantías que el código que no lo hace.

Por ejemplo, suponga que un idioma admite una característica para producir un UInt32de cuatro bytes consecutivos de una alineación arbitraria UInt8[], interpretada en forma big-endian. En algunos compiladores, uno podría escribir el código como:

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

y hacer que el compilador genere una operación de carga de palabras seguida de una instrucción inversa de bytes en palabras. Sin embargo, algunos compiladores no admitirían el modificador __packed y, en su ausencia, generarían código que no funcionaría.

Alternativamente, uno podría escribir el código como:

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

dicho código debería funcionar en cualquier plataforma, incluso en aquellas en las que CHAR_BITSno hay 8 (suponiendo que cada octeto de datos de origen termine en un elemento de matriz distinto), pero es probable que dicho código no se ejecute tan rápido como lo haría el no portátil versión en plataformas compatibles con el primero.

Tenga en cuenta que la portabilidad a menudo requiere que el código sea extremadamente liberal con los tipos de letra y construcciones similares. Por ejemplo, el código que desea multiplicar dos enteros sin signo de 32 bits y producir los 32 bits más bajos del resultado debe escribirse como:

uint32_t result = 1u*x*y;

Sin eso 1u, un compilador en un sistema donde INT_BITS oscilaba entre 33 y 64 podría legítimamente hacer lo que quisiera si el producto de x e y fuera mayor que 2,147,483,647, y algunos compiladores son propensos a aprovechar tales oportunidades.

Super gato
fuente
1

Tiene algunas respuestas excelentes anteriormente, pero dado que, en un comentario, respondió la pregunta "¿Por qué quiere crear un lenguaje de programación propio en primer lugar?" Con "Sería principalmente para el aprendizaje" Voy a responder desde un ángulo diferente.

Tiene sentido escribir un convertidor que tome el código fuente y lo convierta a código C o C ++, de modo que pueda usar un compilador existente como gcc para terminar con el código de máquina, si está más interesado en aprender sobre léxico, sintaxis y ¡análisis semántico de lo que está aprendiendo sobre la generación y optimización de código!

Escribir su propio generador de código de máquina es un trabajo bastante significativo que puede evitar compilando en código C, ¡si no es lo que le interesa principalmente!

Sin embargo, si estás en el programa de ensamblaje y te fascinan los desafíos de optimizar el código en el nivel más bajo, entonces, por supuesto, ¡escribe un generador de código para la experiencia de aprendizaje!

Carson63000
fuente
-7

Depende de qué sistema operativo esté utilizando si está utilizando Windows, existe un IL (lenguaje intermedio) de Microsoft que convierte su código en un lenguaje intermedio para que no se tarde en compilar el código de la máquina. O si está utilizando Linux, hay un compilador separado para eso

Volviendo a su pregunta es cuando al diseñar su propio idioma debe tener un compilador o intérprete separado para eso porque la máquina no conoce el lenguaje de alto nivel. Su código debe compilarse en código de máquina para que sea útil para la máquina

Tayyab Gulsher Vohra
fuente
2
Your code should be compiled into machine code to make it useful for machine- Si su compilador produjo código c como salida, podría poner el código c en el compilador ac para producir código de máquina, ¿verdad?
Robert Harvey
sí. porque la máquina no tiene el lenguaje c
Tayyab Gulsher Vohra
2
Correcto. Entonces la pregunta era "¿Cuándo tiene sentido emitir cy usar el compilador ac, en lugar de emitir directamente lenguaje de máquina o código de bytes?"
Robert Harvey
En realidad, está pidiendo diseñar su lenguaje de programación en el que está pidiendo que "lo convierta a código C o C ++". Así que estoy explicando esto si estás diseñando tu propio lenguaje de programación por qué deberías usar el compilador de c o c ++. si eres lo suficientemente inteligente, deberías diseñar el tuyo propio
Tayyab Gulsher Vohra
8
No creo que entiendas la pregunta. Ver yosefk.com/blog/c-as-an-intermediate-language.html
Robert Harvey