Objetivo de aprendizaje
Al concluir este tema, el estudiante será capaz de:
- Distinguir traducir de ejecutar, y definir compilador, intérprete y máquina virtual.
- Describir cómo un intérprete por recorrido del árbol evalúa un programa mediante llamadas recursivas y una pila de llamadas.
- Explicar cómo un compilador genera código de pila a partir del árbol de sintaxis y relacionarlo con la notación posfija.
- Seguir la ejecución de código de pila en una máquina virtual: contador de instrucción, pila, entorno y salida.
- Comparar ambas estrategias (momento de detección de errores, reutilización del código objeto y rendimiento) y clasificar implementaciones reales de lenguajes.
Ruta de trabajo del tema
| Etapa | Contenido | Producto del estudiante |
|---|---|---|
| 1 | Planteamiento inicial | Hipótesis escrita |
| 2 | Traducir y ejecutar: definiciones y comparación | Vocabulario y tabla comparativa |
| 3 | Intérprete por recorrido del árbol | Traza de evaluación explicada |
| 4 | Compilador: generación de código de pila | Código generado y su relación con la notación posfija |
| 5 | Máquina virtual de pila | Seguimiento de pila y entorno |
| 6 | Comparación de estrategias y código objeto | Análisis de cuándo se detecta cada error |
| 7 | Implementaciones híbridas | Clasificación fundamentada |
| 8 | Verificación y reformulación | Cuestionario y respuesta revisada |
1. Planteamiento inicial
Considere dos maneras de que alguien que solo habla español acceda a un libro escrito en otro idioma:
- Un traductor lee el libro completo y produce un libro nuevo, en español. Ese libro puede leerse muchas veces, por muchas personas, sin que el traductor vuelva a intervenir.
- Un intérprete simultáneo traduce en voz alta, frase por frase, mientras el autor habla. La traducción se produce en el momento y debe repetirse, con el intérprete presente, cada vez que se desee escuchar la conferencia.
Los lenguajes de programación enfrentan un problema análogo. El procesador de una computadora solo ejecuta instrucciones elementales (por ejemplo, sumar dos números o copiar un valor de un lugar a otro), mientras que un programa en un lenguaje de alto nivel contiene expresiones como (3 + 4) * 2.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Esta hipótesis se retomará al final de la sección 6.
2. Traducir y ejecutar
Definición — Traducir y ejecutar: traducir es convertir un programa de una representación a otra (por ejemplo, de texto fuente a instrucciones de una máquina). Ejecutar es llevar a cabo las instrucciones de un programa para producir sus efectos y su resultado.
Definición — Compilador, intérprete y máquina virtual: un compilador traduce el programa completo a otra representación, llamada código objeto, antes de ejecutarlo; la ejecución del código objeto es un proceso aparte. Un intérprete ejecuta el programa directamente a partir de su texto o de una representación interna (como el árbol de sintaxis), sin producir un código objeto independiente. Una máquina virtual es un programa que simula una máquina abstracta capaz de ejecutar un conjunto de instrucciones propio (por ejemplo, instrucciones de pila).
Las fases de análisis léxico, sintáctico y semántico, estudiadas en los temas anteriores, son comunes a ambas estrategias: ninguna puede ejecutar un programa que no ha comprendido. Las estrategias se separan después, a partir del árbol de sintaxis abstracta ya verificado:
| Criterio | Compilador (típico) | Intérprete (típico) |
|---|---|---|
| Momento de la traducción | Antes de ejecutar, una sola vez | Durante la ejecución, cada vez que se ejecuta |
| Resultado de la traducción | Código objeto independiente | Ninguno independiente: el programa fuente o su árbol se necesita en cada ejecución |
| Detección de errores | Todos los detectables estáticamente, antes de ejecutar | Cada error se descubre al llegar a la sentencia que lo contiene |
| Ejecución repetida | Reutiliza el código objeto | Vuelve a procesar el programa |
| Ciclo de desarrollo | Compilar y después ejecutar | Ejecutar directamente (iteración rápida) |
Importante: esta tabla describe tendencias, no reglas. Un intérprete puede verificar el programa antes de ejecutarlo y un compilador puede producir código que se interpreta. Lo que distingue a las estrategias es el tipo de artefacto que se produce, y no el hecho de que haya o no verificación previa.
3. Intérprete por recorrido del árbol
Definición — Intérprete por recorrido del árbol: intérprete que ejecuta el programa evaluando directamente su árbol de sintaxis abstracta mediante una función recursiva. Cada nodo se evalúa con una llamada que devuelve su valor; los operadores evalúan primero sus operandos y después aplican su operación.
Caso de estudio. En el tema 1.2.2 se estudió la semántica operacional de pequeño paso: la expresión se reducía poco a poco, escribiendo cada expresión intermedia. El intérprete por árbol realiza la misma evaluación en la modalidad de gran paso: cada llamada devuelve directamente el valor final de su subárbol. El resultado es idéntico; cambia la forma de organizar el cálculo.
Aplicación práctica. En la corrida siguiente, el árbol se evalúa nodo por nodo. Observe tres elementos: la pila de llamadas (qué evaluaciones están pendientes), el orden en que se visitan los nodos (primero el operando izquierdo, después el derecho y por último el operador) y cómo los valores suben desde las hojas hasta la raíz. Active el modo de predicción para anticipar el valor de cada operador.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
4. Compilador: generación de código de pila
Definición — Código de pila: código para una máquina que opera con una pila de valores. Las instrucciones no indican dónde están los operandos: los toman del tope de la pila y dejan allí su resultado. Esto simplifica la generación de código, porque no es necesario asignar registros.
Las instrucciones de la máquina de pila de Mini son las siguientes:
| Instrucción | Efecto |
|---|---|
PUSH v |
Coloca el valor constante v en el tope de la pila |
LOAD x |
Coloca en la pila el valor que el entorno asocia a x |
ADD, SUB, MUL, DIV |
Retira los dos valores superiores (b es el tope y a el siguiente), calcula a ⊕ b y coloca el resultado |
STORE x |
Retira el tope de la pila y lo asocia a x en el entorno |
PRINT |
Retira el tope de la pila y lo escribe en la salida |
Caso de estudio. El generador recorre el árbol en orden posterior: primero genera el código de los operandos y después la instrucción del operador. Para (3 + 4) * 2 produce PUSH 3, PUSH 4, ADD, PUSH 2, MUL. Observe que no existen instrucciones para los paréntesis: la estructura del árbol ya expresa la agrupación, y el orden de las instrucciones la conserva.
Nota: la secuencia de operandos y operadores en orden posterior es la notación posfija (o notación polaca inversa):
3 4 + 2 *. Un compilador a código de pila produce, en esencia, la notación posfija del programa.
Aplicación práctica. La corrida siguiente muestra cómo el generador visita el árbol y emite una instrucción por nodo. En ella no se ejecuta nada: solo se produce el programa traducido.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
5. Máquina virtual de pila
Definición — Máquina virtual de pila: programa que ejecuta código de pila. Su estado está formado por un contador de instrucción (qué instrucción sigue), una pila de valores, un entorno y una salida. Ejecutar consiste en repetir: tomar la instrucción actual, modificar el estado y avanzar el contador.
Caso de estudio. La máquina no conoce el texto fuente ni el árbol: solo ve una lista de instrucciones elementales. Su ciclo es muy simple y por eso puede implementarse con eficiencia en cualquier plataforma. Las máquinas virtuales de Java y de CPython (la implementación habitual de Python) son máquinas de pila.
Aplicación práctica. Ejecute un programa completo de Mini: primero se compila y después la máquina ejecuta el código objeto. Observe cómo LOAD y PUSH hacen crecer la pila, cómo los operadores la reducen y cómo STORE y PRINT la vacían. Active el modo de predicción para anticipar el valor del tope o el tamaño de la pila.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Nota: compare con la sección 3. En el intérprete por árbol, las evaluaciones pendientes se guardan en la pila de llamadas del lenguaje en que está escrito el intérprete (recursión implícita). En la máquina de pila, lo pendiente se guarda en una pila explícita de valores que el propio programa manipula. La recursión se ha transformado en una estructura de datos visible: es un ejemplo de abstracción hecha explícita.
6. Comparación de las estrategias
6.1 ¿Cuándo se detecta cada error?
Se procesa el mismo programa con ambas estrategias. El caso «Error semántico tardío» contiene un error en la tercera sentencia; el caso «División entre cero» contiene un error que depende de un valor.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Conclusión. Un compilador que verifica el programa completo puede rechazarlo antes de ejecutar nada, lo que evita que el usuario final encuentre la falla. Sin embargo, solo puede anticipar los errores que no dependen de valores concretos. Los errores de ejecución, como la división entre cero con un divisor calculado, aparecen en ambas estrategias en el mismo punto.
6.2 Código objeto: traducir una vez, ejecutar muchas veces
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
El código objeto es un artefacto independiente: se traduce una vez y puede ejecutarse tantas veces como se desee, con distintos datos, incluso cuando el programa fuente ya no está disponible. Esto es lo que permite distribuir un programa sin entregar su código fuente. Un intérprete, en cambio, necesita el programa fuente (o su árbol) en cada ejecución.
6.3 Rendimiento y criterios de elección
La ejecución del código objeto suele ser más rápida, porque se evita volver a analizar el programa y recorrer su árbol en cada ejecución; con compilación a código máquina, la diferencia es mayor. En los programas diminutos de este tema esa diferencia no puede apreciarse. La elección entre estrategias depende del contexto:
| Favorece la compilación cuando… | Favorece la interpretación cuando… |
|---|---|
| Se necesita máximo rendimiento (sistemas embebidos, videojuegos, cómputo de alto desempeño) | Se prioriza la velocidad de desarrollo y la iteración (prototipado, scripts) |
| El entorno de despliegue es conocido y estable | Se requiere portabilidad sin recompilar |
| Se desea detectar errores antes de entregar el software | Se trabaja en entornos interactivos (consolas, cuadernos de datos) |
Contraste con el planteamiento inicial
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
7. Implementaciones híbridas
Definición — Bytecode, máquina virtual y compilación JIT: el bytecode es código intermedio para una máquina virtual (no para un procesador real). La compilación JIT (just in time) consiste en traducir a código máquina, durante la ejecución, las partes del programa que se usan con mayor frecuencia.
Muchas implementaciones modernas combinan las dos estrategias: un compilador traduce el programa fuente a bytecode y una máquina virtual lo ejecuta, a veces con compilación JIT:
flowchart LR
A["Código fuente"] --> B["Compilador a bytecode<br/>(javac, CPython, motor de JavaScript)"]
B --> C["Bytecode"]
C --> D["Máquina virtual<br/>(intérprete de bytecode)"]
C -. "partes frecuentes" .-> E["Compilación JIT<br/>a código máquina"]
D --> F["Ejecución"]
E --> F
Caso de estudio. Java se compila con javac a bytecode, que la máquina virtual interpreta y compila parcialmente en tiempo de ejecución. C# se compila a un lenguaje intermedio (CIL) que ejecuta el entorno CLR. CPython compila el código fuente a bytecode y lo ejecuta en su máquina virtual. Los motores de JavaScript de los navegadores generan bytecode y compilan a código máquina las funciones más utilizadas.
Aplicación práctica. Clasifique cada implementación. Preste atención al enunciado: se clasifica la implementación, no el lenguaje.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
Ejercicio de exploración (opcional): en Python existe el módulo
dis, que muestra el bytecode de una expresión. Ejecuteimport disy despuésdis.dis("(a + b) * 2"). Compare el resultado con el código de pila que generó el laboratorio de la sección 4. Después pruebe condis.dis("(3 + 4) * 2"): es probable que observe que el compilador de CPython ya calculó el resultado durante la traducción (una optimización denominada plegado de constantes). El detalle exacto de las instrucciones depende de la versión de Python.
8. Relación entre los niveles
Con este tema se completa la cadena de transformaciones que va del texto de un programa a su resultado:
flowchart TD
A["Texto del programa"] --> B["Análisis léxico (1.2.1)"]
B --> C["Tokens"]
C --> D["Análisis sintáctico (1.2.1)"]
D --> E["Árbol de sintaxis abstracta"]
E --> F["Análisis semántico (1.2.2)"]
F --> G["Árbol verificado"]
G --> H["Intérprete por árbol (1.2.4)"]
G --> I["Generador de código (1.2.4)"]
I --> J["Código de pila"]
J --> K["Máquina virtual (1.2.4)"]
H --> L["Resultado"]
K --> L
Las cuatro preguntas que organizan la unidad quedan así respondidas:
| Nivel | Pregunta | Herramienta de Mini |
|---|---|---|
| Léxico | ¿Qué unidades contiene el texto? | Autómata finito |
| Sintaxis | ¿Qué estructura tiene? | Gramática y árbol de sintaxis |
| Semántica | ¿Qué significa? | Tipos, entorno y reducciones |
| Traducción y ejecución | ¿Cómo se produce el resultado? | Intérprete por árbol, o código de pila y máquina virtual |
9. Errores conceptuales frecuentes
Error común: afirmar que «un lenguaje es compilado o interpretado». Es una propiedad de la implementación: existen intérpretes de C y compiladores de Python, y la mayoría de las implementaciones actuales son híbridas.
Error común: pensar que el intérprete «no traduce nada». Un intérprete necesita analizar el programa y construir una representación interna (árbol o bytecode) antes de poder ejecutarlo.
Error común: confundir bytecode con código máquina. El bytecode es código para una máquina virtual; solo la máquina virtual (o una compilación JIT) lo lleva a instrucciones del procesador.
Error común: creer que un compilador detecta todos los errores antes de ejecutar. Los errores que dependen de valores (por ejemplo, un divisor calculado) solo aparecen en la ejecución.
Error común: atribuir la velocidad de un programa solo al lenguaje. Depende también de la implementación, del algoritmo y del tipo de problema, y no puede inferirse únicamente del lenguaje utilizado.
10. Verificación del tema
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
11. Libreta de trabajo
Reúna sus respuestas, revíselas y expórtelas como evidencia para el portafolio.
Este laboratorio interactivo requiere JavaScript habilitado en el navegador.
12. Preguntas de reflexión
- ¿En qué escenarios conviene un intérprete y en cuáles un compilador? Justifique con los criterios estudiados.
- ¿Por qué un compilador puede detectar ciertos errores antes de ejecutar y otros no? Proporcione un ejemplo de cada tipo.
- En el intérprete por árbol, la pila de llamadas del lenguaje anfitrión guarda las evaluaciones pendientes. ¿Qué estructura cumple esa función en la máquina de pila y qué ventaja tiene hacerla explícita?
- ¿Qué ventajas ofrece una máquina virtual con bytecode frente a compilar directamente a código máquina? ¿Qué costo tiene?
- Un estudiante afirma: «Python es lento porque es un lenguaje interpretado». ¿Qué matices haría a esa afirmación con lo estudiado en este tema?
- Con los cuatro temas de la unidad, describa el recorrido completo de la línea
total = precio * 3 + 10;desde el texto hasta el resultado, indicando qué hace cada fase.
13. Lecturas recomendadas
- Sebesta, R. (2015). Concepts of programming languages (11th ed.), capítulo introductorio, sección sobre métodos de implementación (compilación, interpretación pura e implementación híbrida). Pearson.
- Scott, M. (2015). Programming language pragmatics (4th ed.), capítulo introductorio sobre la visión general de la compilación. Morgan Kaufmann.
- Nystrom, R. (2021). Crafting Interpreters: el capítulo «A Map of the Territory» ofrece una visión panorámica de las fases, y la segunda parte del libro construye un intérprete por árbol y la tercera una máquina virtual de bytecode. Acceso libre en craftinginterpreters.com.
- Thain, D. (2020). Introduction to Compilers and Language Design (2nd ed.), capítulos introductorios sobre la estructura de un compilador. Acceso libre en www3.nd.edu/~dthain/compilerbook.