Conozcamos un programa que puede crear otros programas para crear aún más programas.
Un compilador es un programa que traduce el código fuente de un lenguaje de programación a código máquina. Si no se hace esto, el ordenador no entenderá cómo ejecutar las instrucciones del desarrollador. Por eso le damos al compilador líneas de código, y él las compara con su diccionario, tiene en cuenta el contexto y emite un conjunto de ceros y unos.
¿Para qué sirve un compilador?

Hagamos una afirmación audaz: los ordenadores son muy tontos, no entienden el lenguaje humano, ni los lenguajes de programación. Todo lo que saben hacer es recibir señales eléctricas y reaccionar de alguna manera a ellas.
Simplificando, un ordenador es una caja con miles de millones de interruptores. Si mueves unos, sumas dos números; si mueves otros, grabas datos en el disco duro. Y aunque los ordenadores modernos son más complejos desde el punto de vista del hardware, el principio sigue siendo similar.
Cuando escribimos código, usamos palabras comprensibles para las personas, como print
, string
, import
, Procedimiento y Excepción (exceptions
). Su significado nos parece obvio: aquí imprimimos el resultado en pantalla, y allí declaramos una variable de cadena. Pero para un ordenador, estas palabras no significan nada.
El ordenador ve la palabra print
y la percibe exactamente igual que tú percibirías palabras de cualquier idioma que desconoces. No se entiende nada, pero seguramente tienen algún significado. Por lo tanto, el ordenador, al igual que nosotros, necesita un traductor, o un compilador.
El compilador entiende lo que significa la palabra print
, e incluso puede decirle al ordenador cómo procesarla correctamente. De este modo, resuelve tres tareas:
- Analiza la sintaxis de lo escrito;
- Lo analiza;
- Genera código máquina.
El compilador recibe como entrada el código fuente y devuelve un archivo ejecutable, un programa listo para funcionar.
Suena sencillo. Pero hay muchas preguntas sobre los compiladores, por ejemplo, en qué lenguajes se escriben, cómo están estructurados internamente y qué tipos existen. Hablaremos de todo esto en el artículo. Y empezaremos por cómo funcionan los compiladores.
¿Cómo funcionan los compiladores?
Una vez más: para que los ordenadores ejecuten las instrucciones de los programadores, necesitan traductores del lenguaje humano al lenguaje de máquina. Analicemos el proceso de traducción, primero en términos generales, y luego en detalle.
En resumen:
El compilador recibe como entrada un archivo con código en algún lenguaje de programación. Convierte las construcciones del lenguaje a un formato que el ordenador pueda entender y devuelve un archivo que el ordenador pueda ejecutar.
Para convertir el código fuente, el compilador utiliza su propio diccionario con definiciones; por ejemplo, el operador if
se cambia a 11010011100110
, y la suma a 101011
. Lo hace hasta que se acaban todas las líneas del archivo. Se obtiene un archivo ejecutable que tiene este aspecto:
001011011010010101110101010101010100001100001110111100110100001010001001110…
En este formato, al ordenador ya le resulta cómodo leer las instrucciones y ejecutarlas. Esto significa que el compilador ha hecho bien su trabajo.
En detalle: La compilación consta de cinco etapas: análisis léxico, análisis sintáctico, análisis semántico, optimización y generación de código. Analicemos cada etapa.
- Análisis léxico: Es algo así como el análisis de la gramática del lenguaje. Cuando escribimos código, seguimos ciertas reglas, la sintaxis. Por ejemplo, en Java, ponemos un punto y coma entre las instrucciones. Si no lo hacemos, obtendremos un error.
En la etapa de análisis léxico, el compilador comprueba si el código se ajusta a las reglas de un lenguaje de programación específico. Y por ahora no piensa en lo que está escrito, la comprobación se realiza solo según características formales.
- Análisis sintáctico: En esta etapa, el compilador divide el código en pequeños fragmentos, tokens. Cada token es una palabra o un símbolo, por ejemplo,
if
,while
,int
o(
.
A partir de los tokens se construye un árbol de análisis sintáctico, que contiene palabras y símbolos, y que será útil en la siguiente etapa, el análisis semántico. Cada nodo del árbol es una operación, por ejemplo, la suma, o una variable. Normalmente, cuando llegamos a una variable, las ramas ya no se ramifican más.
Veamos cómo se ve este árbol.
Supongamos que tenemos un código simple con la suma de dos números:
x = 5 + 3
Aquí hay cinco tokens: x
, =
, 5
, +
y 3
. No vamos a contar los espacios en blanco. A partir de estos tokens se construye este árbol:
Normalmente, los árboles de análisis sintáctico son mucho más complejos, muchísimo más complejos.
Vemos que en la parte superior se encuentra la operación principal: asignar a la variable x el resultado de la suma de dos números. De ella parten dos ramas: la propia variable x y el símbolo de suma, que se ramifica en los números sumandos.
En el proceso de análisis sintáctico, el compilador no entiende para qué sirve cada uno de los tokens. Por ahora, realiza su trabajo mecánicamente; pensará en la siguiente etapa.
- Análisis semántico: El compilador empieza a pensar en lo que está escrito en el código, analizando el árbol de análisis sintáctico construido. Por ejemplo, si hemos declarado una variable, entiende lo que significa y qué operaciones se pueden realizar con ella.
Además, el compilador en esta etapa puede predecir qué acciones son posibles con la variable. Si ve que tenemos una variable de tipo inmutable, por ejemplo, una constante, al intentar modificar el código, dará un error.
- Optimización: Cuando se analiza la sintaxis y se entiende lo que hace el programa, es el momento de acelerar el código. El compilador busca formas de aumentar la velocidad de ejecución o reducir la cantidad de memoria que ocupa.
El ejemplo más simple de optimización es la multiplicación por cero. Por ejemplo, tenemos un fragmento de código:
int x = sin(126) * cos(54) + tan(78);
int y = x * 0;
Para determinar el valor de la variable y, primero hay que calcular una fórmula compleja para la variable x. Pero nosotros, los humanos, vemos inmediatamente que al multiplicar por cero, el resultado será cero, por lo que no tiene sentido calcular la variable x. El compilador también ve estas cosas y no calculará lo que no es necesario calcular. Simplemente reemplazará estas dos líneas de código por una:
int y = 0;
¿Cómodo, verdad? Pero esto solo funcionará si la variable x no nos es útil en el programa más adelante.
Esto es posible debido a las peculiaridades del funcionamiento del compilador: no ejecuta el código, sino que primero lo lee y busca formas de optimizar el programa.
- Generación de código: La sintaxis está analizada, el análisis está realizado, el código está optimizado: es hora de traducirlo al lenguaje del ordenador. En esta etapa, todas las instrucciones que hemos escrito en el lenguaje de programación se traducen a instrucciones de máquina.
Tras la traducción, obtenemos un archivo ejecutable, por ejemplo, en formato .exe, que podemos ejecutar y comprobar el funcionamiento del programa. Aquí termina la compilación.
¿En qué lenguajes se escriben los compiladores?
Espera, si el compilador traduce el código fuente a código máquina, y él mismo es un programa, ¿en qué lenguaje está escrito entonces? Parece un círculo vicioso.
En realidad, no es tan complicado. Los compiladores se pueden escribir en cualquier lenguaje, ya sea Python o ensamblador. Pero hay un matiz.
El primer compilador se escribió en lenguaje ensamblador, porque los programadores necesitaban simplificarse el trabajo con el código máquina. Funcionan así:
- El desarrollador escribe el código en ensamblador;
- El compilador lo traduce a instrucciones de máquina;
- El ordenador ejecuta estas instrucciones.
Resulta que el compilador en ensamblador es otro programa en el mismo lenguaje que sabe traducir el código. Por ejemplo, sustituye la instrucción jmp
por la cadena 001110111
, que pone en marcha los engranajes necesarios dentro del procesador.
Después aparecieron lenguajes de nivel superior, como C. El compilador para C está escrito en el mismo ensamblador. Funciona de forma similar:
- El desarrollador escribe un programa en C;
- El compilador traduce las instrucciones del lenguaje de programación C a instrucciones de máquina;
- El ordenador ejecuta estas instrucciones.
Luego, hacia arriba en el nivel de los lenguajes de programación. El compilador para C++ está escrito en C, y para JavaScript en C++. Pero si bajamos por la cadena, tarde o temprano llegaremos al ensamblador.
¿Por qué no siempre hay un solo compilador para un lenguaje?
Espera, ¿y por qué los lenguajes de programación tienen varios compiladores? ¿Por qué no usar solo uno?
Para cada lenguaje de programación, el primer compilador suele ser escrito por sus desarrolladores. Por ejemplo, tomemos el lenguaje C.
Su compilador está escrito en ensamblador, y lo hizo Dennis Ritchie. Se basó en el principio de que unas instrucciones del lenguaje deben convertirse en unas instrucciones para el ensamblador, y otras en otras. Pero puede que no fuera la mejor implementación: en algunos lugares el compilador podía funcionar lentamente, y en otros ni siquiera funcionaba. Por eso, otros desarrolladores decidieron escribir sus propias versiones del «traductor» del código en C.
Por ejemplo, alguien podría mirar el código del compilador C y pensar: «¡Pero aquí no hay recolector de basura, qué es esto?!» Y se pondría a escribir su propia versión que corrija todas las fugas de memoria y limpie las variables no utilizadas.
Otro desarrollador podría mirar y pensar: aquí no hay una buena optimización para mis tareas de aprendizaje automático. Y luego iría y escribiría un compilador que convertiría el código en C en estructuras de TensorFlow.
Cada implementación del compilador es necesaria para sus propios fines: para algunos es importante recoger la basura, y para otros tener un código súper rápido que supere a cualquier otro. Esto significa que diferirán en la arquitectura, el lenguaje de programación utilizado, la velocidad de funcionamiento y el propósito. Pero, en general, harán lo mismo: compilar.
Tipos de compiladores
Por desgracia, todavía no existe un compilador universal que pueda traducir el código de cualquier lenguaje de programación a código máquina para todos los dispositivos. Tenemos diferentes sistemas operativos, sus versiones, diferentes arquitecturas de procesadores, etc.
Según las tareas, los compiladores se pueden dividir en varios grupos. Por ejemplo, según la dirección de la traducción del código.
- Compiladores tradicionales: Saben traducir el código en lenguaje de programación a código máquina. Principalmente de ellos hemos hablado en este artículo. Un ejemplo es el compilador g++ para el lenguaje C++.
- Compiladores cruzados: Estos compiladores funcionan en una plataforma y crean código para otra. A menudo los utilizan los desarrolladores de sistemas integrados, cuya potencia no es suficiente para la compilación independiente. Por ejemplo, en microcontroladores.
Los compiladores cruzados incluyen GCC (GNU Compiler Collection). Admite C++, Objective-C, Java, Fortran y Go, y diferentes arquitecturas de procesadores.
- Transpiladores: Transforman el código fuente de un lenguaje de alto nivel en el código fuente de otro lenguaje de alto nivel. Por ejemplo, el transpilador Babel transforma ECMAScript 2015+ en JavaScript.
- Compiladores inversos: Estos compiladores hacen lo contrario: analizan el código ya compilado e intentan convertirlo en código fuente en un lenguaje de alto nivel. Esto puede ser útil para el análisis o la depuración.
Compilador, intérprete, traductor: ¿cuál es la diferencia?
Los compiladores no son la única forma de traducir el código fuente a código máquina. También hay intérpretes y compiladores JIT. Vamos a explicar brevemente cuáles son las diferencias entre ellos.
- Intérprete: Es como un traductor simultáneo. Lee el código fuente y lo ejecuta línea por línea. El intérprete no crea archivos adicionales ni construye árboles de análisis sintáctico, sino que ejecuta las instrucciones sobre la marcha, traduciéndolas a bytecode. Por ejemplo, así funciona CPython para el lenguaje Python.
- Compilador JIT: Es un híbrido de compilador e intérprete. Empieza a funcionar como un intérprete y ejecuta las instrucciones a medida que lee el código. Pero parte de las instrucciones las traduce a código máquina para utilizarlas en caso de que se repitan en el futuro. Esto acelera el funcionamiento del programa, ya que permite no ejecutar la misma acción repetidamente.
Cabe mencionar por separado el bytecode. Es un código especial que se ejecuta en una máquina virtual. Se puede decir que ocupa una posición intermedia entre el código escrito en lenguaje de programación y el código máquina. Su implementación se puede encontrar en Java o Python.
Ventajas y desventajas de los lenguajes compilados
Veamos una lista de argumentos a favor y en contra de los lenguajes compilados, es decir, aquellos que utilizan compiladores. Ejemplos de estos lenguajes son: C++, Haskell, Fortran, Rust, Swift y Go.
Ventajas:
- Velocidad de ejecución: El compilador traduce el código fuente a código máquina solo una vez. Y luego, todo está optimizado y listo para ejecutarse. Por lo tanto, estos programas funcionan más rápido, ya que el ordenador no tiene que perder tiempo en su traducción repetida.
- Uso eficiente de los recursos: Una de las etapas de la compilación es la optimización del código. Y dado que los compiladores los escriben los creadores del lenguaje o desarrolladores experimentados, el rendimiento de estos programas será alto.
- Código fuente oculto: Esta es una ventaja no obvia, pero es una verdadera ventaja. Una vez que el programa se ha compilado, su código fuente es difícil de entender. Esto ayuda a evitar intrusiones y a proteger los datos.
Desventajas:
- Compilación larga: El proceso de compilación puede llevar mucho tiempo. Para proyectos pequeños no es tan grave, pero cuando la cantidad de líneas de código de un proyecto supera el millón, no se quiere ejecutar la compilación una y otra vez.
- Dificultad para corregir errores: Normalmente, los errores durante la compilación parecen aterradores debido a la confusa descripción del problema. Simplemente, intenta no poner un punto y coma en un archivo C++ y asegúrate de que no has visto nada peor.
- Dependencia de la plataforma: Si compilas un programa para Windows, no se podrá ejecutar en macOS. Por lo tanto, tendrás que utilizar otro compilador y empezar el proceso de nuevo, o utilizar compiladores cruzados.
Lecturas adicionales
En este artículo solo hemos tratado los principios básicos del funcionamiento de los compiladores. Si quieres entender mejor cómo funcionan, o incluso escribir tu propia versión de traductor al lenguaje de máquina, aquí tienes algunos recursos donde puedes estudiar el tema con más profundidad: