Compilador para niños
En el mundo de la informática, un compilador es un programa especial que toma un código escrito por un programador (llamado código fuente) y lo traduce a otro tipo de código. Este nuevo código se llama código objeto. Imagina que el código fuente es como un libro escrito en un idioma que solo los programadores entienden, y el compilador es el traductor que convierte ese libro a un idioma que la computadora puede entender directamente, como el lenguaje de máquina.
Este proceso de traducción se conoce como compilación. Generalmente, el código fuente está en un lenguaje de alto nivel (más parecido a nuestro idioma), y el código objeto está en un lenguaje de bajo nivel (más cercano a cómo funciona la computadora), como el lenguaje ensamblador o el lenguaje de máquina.
Para construir un compilador, el trabajo se divide en varias partes. Estas partes se agrupan en dos tareas principales: analizar el código original y crear el nuevo código.
- Análisis: Aquí, el compilador revisa que el código fuente esté bien escrito, siguiendo las reglas del lenguaje de programación. Esto incluye:
* Análisis léxico: Descompone el código en pequeñas piezas, como palabras individuales. * Análisis sintáctico: Agrupa esas piezas en frases con sentido, como si formara oraciones. * Análisis semántico: Comprueba que las frases tengan un significado lógico y correcto.
- Síntesis: El objetivo es generar el código final que la computadora entenderá. Esto puede incluir:
* Generación de código: Crea un código intermedio o directamente el código objeto. * Optimización de código: Mejora el código para que el programa funcione de la manera más rápida y eficiente posible, usando menos recursos de la computadora.
También podemos ver estas fases de otra manera:
- Analizador (o front-end): Esta parte se encarga de leer y entender el código fuente. Revisa que todo esté correcto, crea un mapa de cómo está organizado el programa y guarda información importante en una tabla de símbolos. Esta parte suele funcionar igual sin importar para qué tipo de computadora se esté compilando el programa.
- Generador (o back-end): Esta parte toma la información del analizador y crea el código máquina específico para una computadora o sistema operativo en particular.
Esta división es muy útil porque permite que un mismo generador pueda crear código para diferentes tipos de computadoras, y que un mismo analizador pueda entender el código de un lenguaje de programación y prepararlo para varias plataformas.
Contenido
Historia de los compiladores
¿Cuándo surgieron las primeras computadoras?
En 1938, Konrad Zuse creó la primera computadora digital electromecánica, llamada Z1, en Alemania. Más tarde, en 1946, apareció la primera computadora completamente electrónica, la ENIAC, seguida por la EDVAC en 1951. Al principio, estas máquinas solo entendían instrucciones muy básicas, que eran códigos numéricos directos para sus circuitos. Esto se llamaba lenguaje máquina.
De los números a las palabras: el lenguaje ensamblador
Pronto, los primeros usuarios se dieron cuenta de que era muy difícil recordar tantos números. Empezaron a usar claves más fáciles de recordar para escribir sus programas. Luego, esas claves se traducían manualmente al lenguaje máquina. Así nacieron los lenguajes ensambladores. Aunque eran más fáciles, el lenguaje ensamblador seguía siendo muy cercano a cómo funcionaba la máquina.
El nacimiento de los lenguajes de alto nivel
Los investigadores querían crear un lenguaje que fuera aún más sencillo para las personas. La primera persona en escribir un compilador fue Grace Hopper, en 1952, para un lenguaje llamado A-0. En 1950, John Backus en IBM comenzó a trabajar en un lenguaje que permitiera escribir fórmulas matemáticas de forma que la computadora las entendiera. Lo llamaron FORTRAN (FORmulae TRANslator). Fue el primer lenguaje de alto nivel y se lanzó en 1957 para la computadora IBM modelo 704.
Fue entonces cuando surgió la idea de un "traductor" de programas, y si el lenguaje original era de alto nivel y el traducido de bajo nivel, se le llamó "compilador".
Los primeros compiladores: un gran desafío
Crear un compilador al principio era muy complicado. El primer compilador de FORTRAN tardó mucho tiempo en hacerse y era bastante simple. Estaba muy influenciado por la máquina para la que fue creado. Por ejemplo, no le importaban los espacios en blanco porque la máquina que leía los programas (con tarjetas perforadas) no los contaba bien.
El primer compilador que pudo compilar su propio código fuente fue el de Lisp, creado por Hart y Levin en el MIT en 1962. Desde 1970, se hizo común escribir los compiladores en el mismo lenguaje que ellos compilaban, aunque lenguajes como PASCAL y C también fueron muy usados para esta tarea.
Tipos de compiladores
Existen diferentes tipos de compiladores, y algunos pueden pertenecer a varias categorías:
- Compiladores cruzados: Estos crean código para un tipo de computadora diferente a la que están usando para compilar. Por ejemplo, puedes usar tu computadora para crear un programa que funcione en un teléfono móvil.
- Compiladores optimizadores: Hacen cambios en el código para que el programa funcione de forma más eficiente, es decir, más rápido o usando menos memoria, sin cambiar lo que hace el programa.
- Compiladores de una sola pasada: Leen el código fuente una sola vez para generar el código final.
- Compiladores de varias pasadas: Necesitan leer el código fuente varias veces para poder producir el código final.
- Compiladores JIT (just in time): Son parte de un intérprete y compilan partes del código justo en el momento en que se necesitan, mientras el programa se está ejecutando.
Hace mucho tiempo, los compiladores eran considerados programas muy complejos. Los primeros se escribieron directamente en lenguaje máquina o lenguaje ensamblador. Una vez que se tenía un compilador, se podían crear nuevas versiones de compiladores (o incluso otros compiladores) usando el mismo lenguaje que ese compilador podía traducir.
Hoy en día, existen herramientas que ayudan a crear compiladores o intérpretes. Estas herramientas pueden generar la estructura básica de un analizador de código, dejando al programador la tarea de añadir las acciones específicas para cada parte del lenguaje.
El proceso de compilación: paso a paso
El proceso de compilación es cuando las instrucciones de un programa se traducen a un lenguaje que la computadora entiende. Además del compilador, a veces se necesitan otros programas para crear un programa listo para usar. Por ejemplo, un programa grande puede estar dividido en varias partes guardadas en diferentes archivos. Un programa llamado preprocesador puede juntar todas esas partes y expandir abreviaturas o "macros" antes de que el compilador empiece a trabajar.
Normalmente, para crear un programa ejecutable (como un archivo .exe en Windows), se siguen dos pasos:
1. Compilación: En este paso, el compilador traduce el código fuente a un código de bajo nivel (generalmente código objeto, no directamente lenguaje máquina). 2. Enlazado: En este segundo paso, un programa llamado enlazador junta todo el código de bajo nivel que se generó de las diferentes partes del programa. También añade código de las bibliotecas del compilador, que son como colecciones de funciones ya hechas. Esto permite que el programa se comunique con el sistema operativo y finalmente se convierte en un archivo ejecutable en código máquina.
Estos dos pasos se pueden hacer por separado, guardando el resultado de la compilación en archivos temporales, o se puede crear el ejecutable directamente. Un programa incluso puede tener partes escritas en diferentes lenguajes (como C, C++ y Ensamblador), que se compilan por separado y luego se unen para formar un solo programa ejecutable.
Fases internas del proceso
El proceso de traducción dentro del compilador tiene varias etapas o fases que realizan diferentes tareas. Podemos pensar en ellas como piezas separadas, aunque en la práctica a menudo trabajan juntas.
Fase de análisis
Análisis semántico
El análisis semántico revisa el código fuente para encontrar errores de significado y recoge información sobre los tipos de datos. Utiliza la estructura del programa para identificar qué operaciones se están realizando y con qué datos.
Una parte importante de esto es la verificación de tipos. El compilador comprueba si cada operación usa los tipos de datos correctos. Por ejemplo, si intentas usar un número con decimales como índice para encontrar un elemento en una lista, el compilador podría indicar un error. También revisa que las listas o "arreglos" tengan el tamaño correcto.
Fase de síntesis
Esta fase se encarga de generar el código objeto final, pero solo si el código fuente no tiene errores de análisis. El resultado puede ser lenguaje de máquina o lenguaje ensamblador. Se asignan lugares en la memoria para las variables del programa, y cada instrucción intermedia se traduce a una secuencia de instrucciones de máquina. Es clave decidir cómo se guardarán las variables en los registros de la computadora.
Generación de código intermedio
Después de analizar la sintaxis y el significado, algunos compiladores crean una representación intermedia del programa. Esta representación debe ser fácil de crear y fácil de convertir al código final.
Puede tener varias formas, como el "código de tres direcciones", que es similar al lenguaje ensamblador y donde cada instrucción realiza una sola operación. Cada instrucción de tres direcciones tiene como máximo tres elementos (operadores o resultados). El compilador debe crear nombres temporales para guardar los resultados de cada instrucción.
Optimización de código
La fase de optimización de código busca mejorar el código intermedio para que el código final sea más rápido al ejecutarse. Esta fase es muy importante en los compiladores, especialmente en los llamados "compiladores optimizadores", donde se dedica mucho tiempo a esta tarea. Sin embargo, incluso optimizaciones sencillas pueden mejorar mucho la velocidad del programa sin que la compilación tarde demasiado.
Por ejemplo, si el código original tiene una operación que convierte un número entero a un número con decimales, el compilador puede hacer esa conversión una sola vez durante la compilación, eliminando la necesidad de hacerla cada vez que el programa se ejecuta. También puede simplificar cálculos o eliminar pasos innecesarios para hacer el programa más eficiente.
Estructuras de datos principales en un compilador
La forma en que las diferentes partes de un compilador trabajan juntas y cómo guardan la información es muy importante. Los creadores de compiladores buscan que estos programas sean lo más eficientes posible, sin que sean demasiado complicados. Lo ideal es que un compilador pueda traducir un programa en un tiempo que dependa directamente del tamaño del programa.
Componentes léxicos o tokens
Cuando el analizador léxico agrupa los caracteres del código en "tokens" (como palabras o símbolos), los representa de forma simbólica. A veces, también necesita guardar la palabra original o información adicional, como el nombre de una variable o el valor de un número.
En la mayoría de los lenguajes, el analizador léxico solo necesita generar un token a la vez. En otros casos, como en FORTRAN, puede ser necesario guardar una lista de tokens.
Árbol sintáctico
Si el analizador sintáctico crea un árbol sintáctico, este se construye como una estructura que se va formando a medida que se analiza el código. El árbol completo se guarda como una variable que apunta a su nodo principal. Cada nodo en esta estructura es un registro que contiene información recopilada por el analizador sintáctico y, más tarde, por el analizador semántico. Por ejemplo, el tipo de dato de una expresión puede guardarse en el nodo del árbol sintáctico para esa expresión.
A veces, para ahorrar espacio, esta información se guarda de forma dinámica o en otras estructuras de datos, como la tabla de símbolos. Cada nodo del árbol sintáctico puede necesitar diferentes atributos para ser almacenado, dependiendo del tipo de estructura del lenguaje que represente.
Tabla de símbolos
Esta estructura de datos guarda la información relacionada con los nombres que usamos en el programa: funciones, variables, constantes y tipos de datos. La tabla de símbolos es usada por casi todas las fases del compilador: el analizador léxico, sintáctico o semántico pueden añadir nombres; el analizador semántico añade tipos de datos y otra información; y las fases de optimización y generación de código usan esta información para tomar decisiones.
Como la tabla de símbolos se consulta muy a menudo, las operaciones para añadir, eliminar y buscar información deben ser muy rápidas. Una estructura de datos común para esto es la tabla hash, aunque también se pueden usar estructuras de árbol. A veces se usan varias tablas que se organizan en una lista o pila.
Tabla de literales
La búsqueda y adición rápida también son esenciales para la tabla de literales, que guarda las constantes y textos (cadenas de caracteres) usados en el programa. Sin embargo, una tabla de literales no necesita eliminar elementos porque sus datos se aplican a todo el programa, y una constante o texto solo aparecerá una vez en esta tabla. La tabla de literales es importante para reducir el tamaño de un programa en la memoria, ya que permite reutilizar constantes y textos. También es necesaria para que el generador de código cree las direcciones para las constantes y añada las definiciones de datos al archivo de código final.
Código intermedio
Dependiendo del tipo de código intermedio (como el código de tres direcciones) y de las optimizaciones que se realicen, este código puede guardarse como una lista de textos, un archivo de texto temporal o una lista de estructuras conectadas. En los compiladores que hacen optimizaciones complejas, es muy importante elegir representaciones que permitan reorganizar el código fácilmente.
Archivos temporales
Al principio, las computadoras no tenían suficiente memoria para guardar un programa completo durante la compilación. Este problema se solucionaba usando archivos temporales para guardar los resultados de los pasos intermedios, o compilando "sobre la marcha", es decir, guardando solo la información necesaria de las partes anteriores del programa.
Hoy en día, la memoria ya no es un problema tan grande, y es posible que todo el programa se mantenga en memoria. Aun así, los compiladores a veces encuentran útil generar archivos intermedios durante algunas etapas del proceso.
Véase también
En inglés: Compiler Facts for Kids
- BlueJ
- Lenguaje de programación
- Lenguaje ensamblador
- Ensamblador
- Desensamblador
- Decompilador
- Intérprete
- Depurador
- Lenguaje de alto nivel
- Lenguaje de bajo nivel
- Lenguaje de máquina
- Historia de la construcción de los compiladores
- Libros Principles of Compiler Design, Compilers: Principles, Techniques, and Tools