Saltar a contenido

1.4.-Traductores

1.4. Traductores de lenguajes de alto nivel

Idea principal

Un programa escrito en un lenguaje de alto nivel necesita transformarse o procesarse para que el equipo pueda ejecutarlo. La compilación, la interpretación y la compilación JIT son estrategias distintas para recorrer ese camino, con efectos sobre el rendimiento, la portabilidad y las herramientas necesarias.

En los apartados anteriores has diferenciado el código fuente, el código objeto y el código intermedio. Ahora veremos qué herramientas producen o procesan esas representaciones. Entenderlas permite explicar por qué un proyecto en C genera un ejecutable para una plataforma concreta, mientras que una aplicación Java necesita una JVM o una aplicación Python necesita su intérprete.

El objetivo de este tema no es memorizar siglas aisladas, sino seguir el recorrido del programa desde el código fuente hasta su ejecución y justificar qué traductor interviene en cada caso de uso profesional.

Según la normativa del módulo Entornos de Desarrollo, este contenido contribuye al siguiente resultado de aprendizaje y criterios de evaluación:

Código Descripción
RA 1 Reconoce los elementos y herramientas que intervienen en el desarrollo de un programa informático, analizando sus características y las fases en las que actúan hasta llegar a su puesta en funcionamiento.
CE 1.c Se han diferenciado los conceptos de código fuente, objeto y ejecutable.
CE 1.d Se han reconocido las características de la generación de código intermedio para su ejecución en máquinas virtuales.

Qué deberías saber al terminar

Al acabar este apartado deberías poder:

  • diferenciar compilación anticipada, interpretación, transpilación y compilación JIT;
  • describir las fases de análisis y síntesis de un compilador;
  • relacionar código fuente, código intermedio, código máquina y ejecutable con las herramientas que los producen o ejecutan;
  • explicar qué papel desempeñan el frontend, el optimizador y el backend de un compilador.

Mapa del tema

En este documento seguiremos esta secuencia:

  1. qué hace un traductor y cómo funciona la compilación;
  2. qué fases y componentes tiene un compilador;
  3. cómo funciona la interpretación;
  4. cómo mejora el rendimiento la compilación JIT.

1. Compilación

Un compilador transforma un programa escrito en un lenguaje de programación en otra representación que permita continuar su ejecución o distribución. El caso más conocido es la compilación a código máquina, que produce un ejecutable para una plataforma determinada. Cuando esta traducción se completa antes de arrancar el programa se denomina compilación anticipada o AOT (Ahead-Of-Time).

Compilación AOT

Es la traducción del código fuente a código nativo antes de ejecutar la aplicación. El resultado suele ser un ejecutable o una biblioteca para un sistema operativo y una arquitectura concretos.

Un compilador transforma código fuente en un programa ejecutable
Un compilador puede generar un programa preparado para ejecutarse en una plataforma concreta.

Lenguajes como Fortran, C, C++, Rust o Go suelen generar código nativo antes de la ejecución. Este enfoque permite aplicar optimizaciones costosas durante la construcción del programa y adaptar el resultado a la arquitectura de destino, por ejemplo x86-64 o ARM. A cambio, normalmente hay que generar y distribuir una versión distinta para cada plataforma compatible.

No toda compilación termina en código máquina. Algunos lenguajes, como Java, Kotlin sobre la JVM o C#, generan primero bytecode o código intermedio portable, que después procesa una máquina virtual. Este modelo se estudia con más detalle en el apartado anterior.

También existe la compilación fuente a fuente (source-to-source): el resultado es código fuente en otro lenguaje, no código máquina. A esta transformación se la suele llamar transpilación. Es habitual en desarrollo web porque los navegadores ejecutan JavaScript:

Lenguaje de entrada Resultado habitual Uso representativo
TypeScript JavaScript Aplicaciones web con comprobación de tipos durante el desarrollo
Kotlin/JS JavaScript Interfaces web programadas con Kotlin
Dart JavaScript Aplicaciones web compiladas para navegador
PureScript JavaScript Aplicaciones web con enfoque funcional

TypeScript en un proyecto web

Una persona desarrolladora escribe saludo.ts con anotaciones de tipos. La herramienta tsc comprueba esos tipos y genera saludo.js. El navegador no ejecuta TypeScript: descarga y ejecuta el JavaScript producido.

1.1. El proceso de compilación

La compilación es un proceso complejo que puede agruparse en dos grandes fases: análisis, para comprender y validar el programa, y síntesis, para producir una representación ejecutable o cercana a ella.

Fase Qué hace Resultado o ejemplo
Análisis léxico Agrupa los caracteres del código en tokens: palabras reservadas, identificadores, literales u operadores. int suma = a + b; se convierte en una secuencia de tokens.
Análisis sintáctico Comprueba que los tokens respetan la gramática y construye una representación estructurada. Un AST (Abstract Syntax Tree) refleja que a + b forma parte de una asignación.
Análisis semántico Revisa que el programa tenga sentido: tipos compatibles, nombres declarados y reglas del lenguaje. Detecta que no se puede asignar un número a una variable de tipo texto sin conversión válida.
Generación de código intermedio Produce una representación interna más cómoda para optimizar y generar distintos destinos. Una IR (Intermediate Representation) del compilador.
Optimización Modifica la representación sin alterar el comportamiento observable del programa. Puede simplificar 5 * 10 a 50.
Generación de código objeto y enlazado Genera código nativo y combina el resultado con bibliotecas necesarias. Archivos objeto y, finalmente, un ejecutable.
Fases de análisis y síntesis del proceso de compilación
El análisis valida y estructura el código; la síntesis genera y optimiza el resultado.

Un AST no es un ejecutable

El árbol de sintaxis abstracta es una estructura de datos que representa el programa durante el análisis. No contiene instrucciones que la CPU pueda ejecutar directamente; sirve de base para comprobar el código y generar representaciones posteriores.

1.2. Estructura de un compilador

Los compiladores actuales suelen dividir sus responsabilidades en tres partes. Esta organización permite reutilizar trabajo entre lenguajes y plataformas.

Estructura de un compilador dividida en frontend, optimizador y backend
La separación entre análisis, optimización y generación facilita soportar varios lenguajes y arquitecturas.
Componente Responsabilidad Dependencia principal
Analizador o frontend Realiza los análisis léxico, sintáctico y semántico; transforma el código fuente en una representación intermedia. Lenguaje de origen
Optimizador Mejora la representación intermedia sin cambiar el significado del programa. Puede compartirse entre varios lenguajes y destinos
Generador o backend Convierte la representación intermedia en código para el destino y prepara el enlazado. Arquitectura y plataforma de destino

Por ejemplo, un compilador puede tener un frontend para C y otro para C++, que producen una representación intermedia común. Después, distintos backends generan código para x86-64, ARM o RISC-V. Así no es necesario construir un compilador completo para cada combinación de lenguaje y procesador.

2. Interpretación y lenguajes interpretados

La interpretación es otra forma de ejecutar un programa. En lugar de entregar al sistema operativo un ejecutable nativo completo, un programa llamado intérprete analiza una representación del programa y realiza las operaciones indicadas. Esa representación puede ser el propio código fuente, un árbol de sintaxis abstracta o, con frecuencia, bytecode.

Intérprete

Es un programa que procesa el código fuente o una representación intermedia y realiza las operaciones que indican. Para ejecutar la aplicación, el intérprete debe estar disponible en el sistema.

Un intérprete procesa el código de un programa durante su ejecución
En la interpretación, el intérprete forma parte del entorno necesario para ejecutar la aplicación.

Un intérprete puede recorrer directamente un AST, aunque este enfoque suele tener más sobrecoste. Es muy habitual que primero convierta el código fuente a bytecode y que ejecute ese bytecode. Por ello, clasificar un lenguaje como «compilado» o «interpretado» es una simplificación útil para empezar, pero el comportamiento real depende de la implementación y de su entorno de ejecución.

Python, PHP y Perl cuentan con implementaciones que usan bytecode o estructuras internas para ejecutar los programas. Ruby 1.8 empleaba árboles de sintaxis abstracta; las implementaciones actuales han evolucionado y no todas funcionan igual. En Python, por ejemplo, CPython compila normalmente el código fuente a bytecode y su máquina virtual lo ejecuta. Por tanto, ejecutar python programa.py no significa que la CPU lea el archivo fuente línea a línea.

No confundas el lenguaje con una implementación

Decir que «Python es interpretado» describe el uso habitual de CPython, pero existen otras implementaciones, como PyPy, que incorporan JIT. Para razonar con precisión, indica la herramienta o el entorno de ejecución concreto.

3. Compilación en tiempo de ejecución (JIT)

La compilación JIT (Just-In-Time) combina ideas de la interpretación y de la compilación. Un entorno de ejecución puede comenzar procesando bytecode y, mientras el programa está en marcha, detectar los métodos o fragmentos que se usan con frecuencia. Entonces los traduce a código máquina y conserva ese resultado en memoria para reutilizarlo.

El JIT dispone de información que una compilación AOT no conoce todavía, como qué rutas del programa se ejecutan más, los tipos observados o los valores habituales. Con ella puede optimizar las partes «calientes» del programa. Es una técnica habitual en la JVM de Java y Kotlin, en el CLR de .NET y en motores modernos de JavaScript.

código fuente → bytecode → entorno de ejecución
                              ↓
                    identifica código frecuente
                              ↓
                    JIT → código máquina en memoria → CPU

Una aplicación Java de larga duración

Un servicio Java que procesa muchas peticiones puede ejecutar primero bytecode y, tras detectar que un método de validación se invoca miles de veces, compilarlo con JIT a código nativo. Las llamadas posteriores reutilizan esa versión optimizada mientras siga siendo válida.

La compilación JIT, también llamada dinámica, puede mejorar mucho el rendimiento de una aplicación que se mantiene activa. Sin embargo, no garantiza que sea más rápida que AOT en todos los casos: necesita tiempo de arranque o warm-up, consume recursos para recopilar información y compilar, y sus resultados dependen de la carga real.

4. Elegir y usar traductores con criterio

La estrategia no se elige solo por una etiqueta del lenguaje. Depende de las restricciones del proyecto, del entorno de despliegue y de las herramientas disponibles.

Situación Enfoque habitual Razón principal
Aplicación de sistema o componente con requisitos estrictos de rendimiento y distribución controlada Compilación AOT Genera código nativo específico y permite optimizar antes de distribuir.
Aplicación Java, Kotlin o .NET desplegada en varios sistemas Bytecode o código intermedio más entorno de ejecución Reutiliza el mismo artefacto cuando hay una máquina virtual compatible.
Automatización o herramienta ejecutada con el intérprete instalado Interpretación o bytecode interpretado Facilita un ciclo rápido de edición y ejecución.
Aplicación web escrita en TypeScript Transpilación a JavaScript El navegador recibe el formato que puede ejecutar.
Servicio de larga duración sobre JVM, .NET o un motor JavaScript JIT Puede optimizar el código más usado con datos de ejecución.

5. Buenas prácticas

  • Identifica los artefactos que genera el proyecto: distingue archivos fuente, objetos, bytecode, bibliotecas y ejecutables antes de borrarlos o distribuirlos.
  • Comprueba el entorno de destino: un bytecode portable sigue necesitando una JVM, un CLR o un intérprete compatibles, además de sus bibliotecas y configuración.
  • No deduzcas el modelo de ejecución solo por el nombre del lenguaje: consulta la implementación concreta, por ejemplo CPython, PyPy, JVM o V8.
  • Mide antes de atribuir una mejora al JIT: en aplicaciones de larga duración, separa el tiempo de arranque del rendimiento estable y utiliza herramientas de perfilado.
  • Incluye la transpilación en el proceso de construcción web: distribuye el JavaScript generado, no los archivos TypeScript, cuando el destino sea un navegador.

6. Errores frecuentes

Error frecuente Por qué ocurre Cómo evitarlo
Creer que todo lenguaje es exclusivamente compilado o interpretado. Las etiquetas simplifican modelos que pueden combinar bytecode, intérprete y JIT. Indica el compilador o entorno concreto y qué representación procesa.
Confundir bytecode con código máquina. Ambos son resultados de una traducción, pero el bytecode necesita normalmente un entorno de ejecución. Relaciona el bytecode con la JVM, el CLR o la máquina virtual correspondiente.
Pensar que un transpilador genera un ejecutable. La transpilación produce código fuente en otro lenguaje. Revisa la extensión y el destino: TypeScript genera JavaScript, no un binario nativo.
Atribuir cualquier mejora de rendimiento al JIT. El rendimiento también depende de la entrada/salida, la base de datos, la memoria y la configuración. Perfila la aplicación y compara medidas en condiciones equivalentes.
Confundir un AST con código ejecutable. El AST representa la estructura del programa, pero es una estructura interna de análisis. Sitúalo entre los tokens y la generación de código intermedio o nativo.

7. Resumen

En este tema has aprendido que:

  • la compilación transforma código fuente a otra representación y, en AOT, suele producir código nativo antes de ejecutar;
  • un compilador analiza el código mediante fases léxica, sintáctica y semántica, y después genera, optimiza y enlaza el resultado;
  • un compilador se organiza habitualmente en frontend, optimizador y backend para reutilizar componentes entre lenguajes y plataformas;
  • un intérprete procesa el código fuente o una representación intermedia durante la ejecución;
  • JIT compila en tiempo de ejecución las partes más utilizadas y puede mejorar el rendimiento tras un periodo de warm-up;
  • la transpilación transforma código fuente en otro lenguaje, como TypeScript a JavaScript.

Idea clave

Reconocer qué traductor interviene y qué artefacto genera permite explicar cómo llega un programa a la CPU, qué debe instalarse para ejecutarlo y qué compromisos existen entre rendimiento, portabilidad y rapidez de desarrollo.

8. Para seguir practicando

  • Realiza la Práctica 1.2: Herramientas para generar un programa y compara el proceso de construcción y ejecución de dos lenguajes.
  • Compila un programa Java con javac, ejecuta javap -c NombreClase y localiza el bytecode generado; compáralo con un ejecutable nativo de C o C++.
  • Crea un archivo TypeScript mínimo, ejecútalo con tsc y revisa el archivo

Bibliografía y fuentes

Presentación

ED-U1.4 - Traductores de lenguajes de alto nivel