Vemos a grandes rasgos, que tenemos un programa fuente o source program, este se compila y se traduce a un programa objeto o target program. Lo que analizaremos en este post, será el cuadradito verde oscuro que dice "compilador".
He aquí las diferentes fases de un compilador:
1) Lexical Analyzer o Analizador Léxico
Este proceso se denomina "SCANNING". Se escanean los símbolos de entrada y se agrupan en unidades llamadas tokens o words. Por ejemplo, el siguiente código escrito en Java contiene 5 tokens:
temp = x + 1
token 1 : "temp" (identificador)
token 2: " = " (símbolo de asignación)
token 3: " x " (variable)
token 4: " + " (símbolo de suma)
token 5: " 1 " (número)
2) Syntax Analysis o Análizador Sintáctico
Este proceso se denomina "PARSING". Los tokens son agrupados en unidades sintácticas (como ser expresiones, sentencias y declaraciones) que conforman las reglas gramaticales de los lenguajes de programación. El objetivo del "parsing" es generar una estructura de datos ( parse tree) que represente la estructura sintáctica del programa para luego obtener una respuesta: programas sintacticamente correctos (continua el análisis) o programas sintácticamente incorrectos (produce un error y la compilación finaliza).
Ahora bien, veamos un ejemplo: dada una secuencia de tokens y utilizando la gramática BNF.
s: statement
v: variable
e: expression
b: boolean
Vamos definiendo una secuencia de pasos donde reemplazamos símbolos no terminales por otros símbolos no terminales hasta que finalmente resultan en un símbolo terminal ( x,y,z,0,1,2,3,4).
Este tipo de interpretación gráfica presenta una ambigüedad. Porque???
Porque depende del orden en el que leamos las sentencias.
Sea
s1 = "x := 1"
s2 = "y := 2"
s3 = "if x=y then y:=3"
Veamos que s1;s2;s3 es ambigüo. Es decir, que no resulta lo mismo si ejecutamos de la siguiente forma:
a) ( s1 ; s2 ) ; s3
b) s1 ; ( s2 ; s3 )
Como por ejemplo
¿Qué sucedería si b1 es verdadera y b2 es falsa ? ¿Se ejecutará s2? Como se puede observar, esto depende en como la sentencia se "parsea" o analiza gramaticalmente.
3) Semantic Analysis o Analizador Semántico (Estático)
Este tipo de análisis depende del contexto en el que se analiza una expresión. Por ejemplo
temp := x + 1
Opción 1: declaramos a los identificadores temp y x como números enteros; entonces el número 1 será un número entero y la suma (+) será una suma de enteros
Opción 2: declaramos a x como un número decimal; el número 1 será marcado como un número decimal y la suma (+) será suma de decimales. Ahora bien, ¿qué valor obtiene temp? dependiendo de tipo de dato sea temp. Si temp fue declarado como un número entero, entonces la suma x+1 se redondeará a número entero.
Se observa que el resultado de la suma depende del tipo de dato mediante los cuales fueron definidos tanto el identificador temp como el identificador x.
El proceso de parsing anteriormente mencionado debe incluir información adicional, como ser los tipos de datos de los identificadores y el lugar en el programa donde cada identificador fue declarado.
4) Intermediate Code Generator o Generador de Código Intermedio
Aunque es posible generar un programa destino o program target con los análisis sintácticos y semánticos, resulta dificil generar un código eficiente en una fase. Aún así, muchos compiladores producen un código intermedio y luego optimizan este código para producir un programa destino más eficiente. La representación intermedia puede ser algún tipo de código genérico de bajo nivel que posee propiedades comunes a varias computadoras. ¿Cuál es la ventaja?
Permite la optimización generando código independiente de la máquina.
Existen varios tipos de código intermedio:
Permite la optimización generando código independiente de la máquina.
Existen varios tipos de código intermedio:
- Específico al lenguaje (P-code para Pascal, Bytecodes para Java)
- Código de 3 direcciones ( ej: x := y op z )
- Código de 2 operaciones ( ej: x := op z que es equivalente a x := x op z )
- Representación gráfica
Para mayor información, puede leer las siguientes diapositivas.
5) Code Optimizer o Optimización del Código
Se describen una lista de algunos estándares de optimización para que el programa sea más corto o más rapido en tiempo de ejecución o ambos.
- Eliminación de subexpresiones comunes: si un programa calcula el mismo valor más de una vez y el compilador lo detecta, entonces puede ser posible transformar el programa de manera que el valor sea calculado una sola vez y almacenado para su posterior utilización.
- Propagación de copia: si un programa contiene asignación como por ejemplo x := y, es posible modificar las sentencias posteriores para referirse a "y" en vez de a "x" y de esta forma eliminar la asignación.
- Eliminación de código muerto: si existe alguna secuencia de instrucciones que nunca va a ser ejecutada, entonces se elimina del programa.
- Optimización de bucles: existen varias técnicas que pueden ser aplicadas para eliminar instrucciones de bucles. Por ejemplo, si una expresión aparece dentro de un bucle, pero tiene el mismo valor en cada entrada al bucle, entonces esta expresión se puede mover fuera del bucle.
- Llamadas a funciones: si un programa llama a una función "f", es posible sustituir el código de la llamada a la función por la función completa.
6) Code Generation o Generación de Código
La última fase implica convertir el código intermedio en código de máquina. Esto significa elegir las ubicaciones de memoria, un registo o ambos, para cada variable que aparezca en el programa. Existe una gran cantidad de algoritmos de asignación de registros con el propósito de reusar los registros eficientemente. Esto es importante porque varios diseños de máquinas contienen un número fijo de registros, y las operaciones con registros son más eficientes que la transferencia de datos dentro y fuera de memoria.
Bibliografía:
. Concepts in Programming Languages - John C. Mitchell





















