En informática , la generación de código es parte de la cadena de procesos de un compilador y convierte la representación intermedia del código fuente en una forma (por ejemplo, código de máquina ) que puede ser fácilmente ejecutada por el sistema de destino.
Los compiladores sofisticados suelen realizar múltiples pasadas sobre varias formas intermedias. Este proceso de múltiples etapas se utiliza porque muchos algoritmos para la optimización de código son más fáciles de aplicar uno a la vez, o porque la entrada a una optimización depende del procesamiento completado realizado por otra optimización. Esta organización también facilita la creación de un único compilador que puede apuntar a múltiples arquitecturas, ya que solo la última de las etapas de generación de código (el backend ) necesita cambiar de un objetivo a otro. (Para obtener más información sobre el diseño de compiladores, consulte Compilador ).
La entrada al generador de código consiste típicamente en un árbol de análisis sintáctico o un árbol de sintaxis abstracta . [1] El árbol se convierte en una secuencia lineal de instrucciones, normalmente en un lenguaje intermedio como el código de tres direcciones . Las etapas posteriores de compilación pueden o no denominarse "generación de código", dependiendo de si implican un cambio significativo en la representación del programa. (Por ejemplo, un paso de optimización de mirilla probablemente no se llamaría "generación de código", aunque un generador de código podría incorporar un paso de optimización de mirilla).
Además de la conversión básica de una representación intermedia en una secuencia lineal de instrucciones de máquina, un generador de código típico intenta optimizar el código generado de alguna manera.
Las tareas que normalmente forman parte de la fase de "generación de código" de un compilador sofisticado incluyen:
La selección de instrucciones generalmente se lleva a cabo haciendo un recorrido recursivo de postorden en el árbol de sintaxis abstracta, haciendo coincidir configuraciones particulares del árbol con plantillas; por ejemplo, el árbol podría transformarse en una secuencia lineal de instrucciones generando recursivamente las secuencias para y , y luego emitiendo la instrucción .W := ADD(X,MUL(Y,Z))
t1 := X
t2 := MUL(Y,Z)
ADD W, t1, t2
En un compilador que utiliza un lenguaje intermedio, puede haber dos etapas de selección de instrucciones: una para convertir el árbol de análisis en código intermedio y una segunda fase mucho más adelante para convertir el código intermedio en instrucciones del conjunto de instrucciones de la máquina de destino. Esta segunda fase no requiere un recorrido del árbol; se puede hacer de forma lineal y, por lo general, implica un simple reemplazo de las operaciones del lenguaje intermedio con sus códigos de operación correspondientes . Sin embargo, si el compilador es en realidad un traductor de lenguaje (por ejemplo, uno que convierte Java a C++ ), entonces la segunda fase de generación de código puede implicar la construcción de un árbol a partir del código intermedio lineal.
Cuando la generación de código se produce en tiempo de ejecución , como en la compilación justo a tiempo (JIT), es importante que todo el proceso sea eficiente con respecto al espacio y al tiempo. Por ejemplo, cuando se interpretan y se utilizan expresiones regulares para generar código en tiempo de ejecución, a menudo se genera una máquina de estados finitos no determinista en lugar de una determinista, porque normalmente la primera se puede crear más rápidamente y ocupa menos espacio de memoria que la segunda. A pesar de que generalmente genera un código menos eficiente, la generación de código JIT puede aprovechar la información de creación de perfiles que solo está disponible en tiempo de ejecución.
La tarea fundamental de tomar la entrada en un lenguaje y producir la salida en un lenguaje no trivialmente diferente puede entenderse en términos de las operaciones de transformación centrales de la teoría del lenguaje formal . En consecuencia, algunas técnicas que se desarrollaron originalmente para su uso en compiladores se han empleado también de otras maneras. Por ejemplo, YACC (Yet Another Compiler-Compiler ) toma la entrada en formato Backus–Naur y la convierte en un analizador sintáctico en C. Aunque se creó originalmente para la generación automática de un analizador sintáctico para un compilador, yacc también se utiliza a menudo para automatizar la escritura de código que necesita modificarse cada vez que se cambian las especificaciones. [3]
Muchos entornos de desarrollo integrados (IDE) admiten alguna forma de generación automática de código fuente , a menudo utilizando algoritmos en común con los generadores de código de compilador, aunque por lo general son menos complicados. (Véase también: Transformación de programas , Transformación de datos ).
En general, un analizador sintáctico y semántico intenta recuperar la estructura del programa a partir del código fuente, mientras que un generador de código utiliza esta información estructural (por ejemplo, los tipos de datos ) para producir código. En otras palabras, el primero agrega información mientras que el segundo pierde parte de la información. Una consecuencia de esta pérdida de información es que la reflexión se vuelve difícil o incluso imposible. Para contrarrestar este problema, los generadores de código a menudo incorporan información sintáctica y semántica además del código necesario para la ejecución.
generación de código.