Voy a dar un golpe en esto, pero rellene su taza primero, porque va a ser un poco larga.
Al principio, el programa fuente es solo una secuencia de caracteres individuales, leídos uno a la vez. Antes de que el compilador pueda entenderlo, debe dividirse en palabras, lo que para un compilador es una secuencia de pares con un valor de token y un lexema . Para saber cuántos caracteres van en la siguiente palabra, el front-end del compilador contendrá un escáner , que es más fácil de crear a partir de una combinación de un conjunto de autómatas finitos deterministas (DFA). Para darle una idea de cómo son, aquí hay una imagen de uno que reconoce números, en forma de una parte entera, y una parte decimal opcional, por lo que “142” y “3.141593” son números de acuerdo con este.
Los círculos verdes (estados) están marcados de esa manera porque están “aceptando” estados, es decir, si llegamos a ellos y sucede algo más, sabemos que lo que sucedió antes era al menos un número. Comenzando desde (inicio), un rastro de la ruta tomada al leer “42.15” sería (1,2,2,3,3,3), y dado que eso termina en un estado de aceptación, esa cadena se puede emitir como un par (NÚMERO, 42.15) cuando finaliza la cadena de dígitos. Aquí, NUMBER es un token entero arbitrario que se utiliza para indicar que hemos encontrado números, y el texto real “42.15” es el lexema que coincide con el token, que es necesario para distinguir un número de otro.
Se pueden hacer pequeñas máquinas de estado como esta para todo tipo de clases de palabras, los nombres de las variables se pueden especificar como cadenas de letras y guiones bajos, los operadores pueden ser cosas como “+ =” y “->”, las palabras clave son cosas como ” if “y” while “, los especificadores de tipo pueden ser” int “y” char “y what-have-you. Al hacer un DFA que acepta cada clase de palabras, se pueden fusionar en una más grande que acepta múltiples clases. Podemos representar DFA usando tablas en software, por lo que la construcción del DFA gigante para todas las clases de palabras en un idioma completo puede hacerse mediante un generador de escáner automático que tome descripciones de las clases y produzca un programa que las conozca todas entre sí. Ese fue el paso 1, llamado análisis léxico . Lo que obtienes de él es una nueva secuencia de datos, pero ya no son caracteres, son pares (token, lexeme) que se pueden manejar más fácilmente.
Tomemos una declaración simple como
if (x == 2) {x = a + b; }
y conviértalo en un ejemplo de flujo de tokens y lexemas:
(PALABRA CLAVE, “if”), (IDENTIFICADOR, “x”), (OPERADOR, “==”), (NÚMERO, “2”), (DELIMITADOR, “{“), (IDENTIFICADOR, “x”), ( OPERADOR, “=”), (IDENTIFICADOR, “a”), (OPERADOR, “+”), (IDENTIFICADOR, “b”), (DELIMITER, “;”), (DELIMITER, “}”).
Este es un poco más detallado de lo necesario, pero entiendes la idea.
El siguiente es el análisis sintáctico (también conocido como análisis ), que tiene el objetivo de convertir esta secuencia en un árbol de sintaxis, como este:
La cantidad de formas de producir esta transformación puede llenar un libro, pero una muy simple es mediante el análisis predictivo, lo que básicamente significa que comienza desde un extremo de la secuencia, crea un árbol temporal con marcadores de posición para todas las cosas que no tiene visto todavía, y espero completarlos a medida que continúe leyendo desde la transmisión (preparado con precaución para detener un “error de sintaxis” si lo que viene después no encaja después de todo).
Para nuestro pequeño if-tree, eso podría proceder de la siguiente manera:
Dejo de lado el resto de este proceso, porque probablemente puedas ver a dónde va en este momento.
Habiendo obtenido todo el árbol de sintaxis, necesitamos construir una tabla de símbolos , para decidir lugares para todo en la memoria. Esto significa ir a través del árbol y colocar todos los diferentes nombres de variables en ubicaciones de memoria que sabemos que están disponibles. El compilador decide cómo se gasta la memoria asignada a un programa, por lo que es solo cuestión de encontrar algunas direcciones que aún no se han utilizado para nada, empiezo en 2000 porque es un buen número:
Básicamente, esto es lo que hace que ‘x’ se refiera al mismo número en ambos lugares del árbol donde aparece. Esto es suficiente para una simple traducción al ensamblaje; yendo a través del árbol de izquierda a derecha y de arriba a abajo nuevamente, encontrando un nodo “si” siempre resulta en el mismo patrón, un nodo “verificar igual” siempre da lo mismo, y “asignación”, y así sucesivamente:
Primero, encuentra las expresiones más pequeñas …
… fusionarlos con lo que los rodea …
… y al final, todo se puede aplanar en una secuencia de instrucciones de montaje:
… que se envía al ensamblador para traducir cosas como ‘cargar’, ‘agregar’, etc. a la representación binaria del procesador.
Esto se simplifica enormemente, y el lenguaje ensamblador inexistente se inventa para mantenerlo fácil, pero así es aproximadamente como funciona un esquema de traducción simple, conceptualmente hablando.
Voy a omitir las optimizaciones, porque cada una de las que se me ocurre requiere al menos 3 veces más material de fondo que el que está aquí. Espero que tenga más o menos sentido, para todos los temas pasados; cubriendo todo es un libro con cientos de páginas.