Clase 1 — Introducción al Proceso de Compilación
Resumen Ejecutivo
La sesión presenta la asignatura de Procesadores de Lenguajes, cuyo objetivo es comprender cómo se construye un compilador: desde la lectura del código fuente hasta la generación de código máquina. Se introducen las tres fases fundamentales del análisis (léxico, sintáctico y semántico) y se ofrece una visión global del pipeline completo de compilación. Se establece que se trabajará con Java, JFlex y CUP para construir un compilador del lenguaje didáctico "Pocket".
Conceptos Clave
- Procesador de lenguaje: Programa que lee código escrito en un lenguaje de programación, lo analiza (léxico, sintáctico, semántico) y lo traduce a instrucciones que la máquina pueda ejecutar. ⚠️ EXAMEN
- Compilador: Analiza todo el programa fuente, detecta errores y genera un programa objeto (código máquina) que se puede ejecutar posteriormente sin recompilar.
- Intérprete: Analiza y ejecuta el programa al mismo tiempo, instrucción por instrucción; cada ejecución repite todo el proceso de análisis. ⚠️ EXAMEN
- Token: Unidad sintáctica mínima que el analizador léxico pasa al sintáctico (identificador, palabra reservada, delimitador, constante, operador). ⚠️ EXAMEN
- Tabla de símbolos: Estructura de datos central que almacena información sobre identificadores (variables, funciones): nombre, tipo, ámbito, tamaño, etc. Es utilizada por todas las fases del compilador. ⚠️ EXAMEN
- Bytecode: Código intermedio de bajo nivel (pero superior al ensamblador) independiente de la máquina; en Java es interpretado por la JVM.
- Gramática independiente del contexto: Formalismo que define las reglas sintácticas de un lenguaje de programación, reconocida por autómatas con pila.
- Expresión regular: Formalismo que define los patrones léxicos (formación de palabras/tokens), reconocida por autómatas finitos.
- Lenguaje Pocket: Lenguaje didáctico reducido (escalares, arrays, expresiones aritméticas/lógicas, condicionales, iterativas, E/S) usado para construir el compilador de la asignatura.
Desarrollo del Temario
1. ¿Qué es un procesador de lenguaje?
Un procesador de lenguaje es una aplicación escrita en un lenguaje de alto nivel (en este caso Java) que lee un programa escrito en un lenguaje específico, lo analiza en tres niveles (léxico, sintáctico, semántico) y lo traduce a un código que la máquina pueda ejecutar. Cada compilador es específico para un lenguaje; no existe un compilador universal. ⚠️ EXAMEN
Existen dos grandes tipos de procesadores:
Compilador: Lee el programa completo, realiza todas las fases de análisis y genera un programa objeto (código máquina o intermedio). Una vez compilado, el programa se puede ejecutar múltiples veces sin recompilar. Ejemplos: C, C++, Pascal, Ada.
Intérprete: Analiza y ejecuta simultáneamente, instrucción por instrucción. Cada vez que se ejecuta el programa, se repite todo el proceso de análisis. Ejemplos: Prolog, Lisp, Python. La ejecución es más lenta porque repite el análisis en cada ejecución. ⚠️ EXAMEN
Ejemplo: Java usa un sistema híbrido: el compilador genera bytecode (código intermedio independiente de la máquina), que luego es interpretado por la JVM (Java Virtual Machine). La fase de compilación garantiza que no se producirán ciertos errores en ejecución. Da igual en qué máquina se compiló: con que exista una JVM para el hardware destino, el bytecode se ejecuta correctamente.
Los compiladores deben ser deterministas: no pueden existir compiladores no deterministas porque provocarían errores en la toma de decisiones al elegir qué reglas aplicar. Si hay ambigüedad, existen criterios para resolverla, pero la gramática debe estar bien escrita. ⚠️ EXAMEN
2. Visión global del proceso de compilación
El proceso completo sigue este pipeline:
Código fuente → [Analizador de código] → [Generador de código] → Código objeto
├─ Léxico ├─ Código intermedio
├─ Sintáctico ├─ Optimización
└─ Semántico └─ Código máquina
↕
Tabla de Símbolos
Solo el analizador léxico lee el código fuente directamente. Las demás fases trabajan con las estructuras que este produce (tokens, tabla de símbolos). Esto es crucial para la eficiencia: evita releer millones de líneas de código en cada fase. ⚠️ EXAMEN
La interacción entre fases es la siguiente: el sintáctico es el que manda, pidiendo tokens al léxico conforme los necesita. El léxico lee palabra por palabra y devuelve el tipo de token leído. ⚠️ EXAMEN
3. Fase 1: Análisis Léxico (Scanner / Analizador Morfológico)
El analizador léxico (también llamado scanner o analizador morfológico) transforma el código fuente en una secuencia de tokens. Sus funciones son:
- Leer el código carácter a carácter y agrupar en palabras/elementos.
- Clasificar cada elemento en su tipo de token: identificador, palabra reservada, constante numérica, operador, delimitador.
- Eliminar comentarios, espacios en blanco, tabuladores y saltos de línea.
- Detectar errores léxicos: símbolos no permitidos (ej:
$), identificadores mal formados (ej: empezar por número), constantes mal construidas, comentarios mal cerrados. - Acceder a la tabla de símbolos para registrar variables.
Los patrones léxicos se definen mediante expresiones regulares, que por debajo se implementan con autómatas finitos. ⚠️ EXAMEN
Ejemplo: El código
main { int a; a = 100; }se transforma en la secuencia de tokens:[main (palabra reservada), { (delimitador), int (palabra reservada), identificador(a), ; (delimitador), identificador(a), = (operador), 100 (constante entera), ; (delimitador), } (delimitador)]. Al sintáctico le pasa "identificador", no "a" — la referencia concreta queda en la tabla de símbolos.
4. Fase 2: Análisis Sintáctico (Parser)
El analizador sintáctico (parser) recibe la secuencia de tokens del léxico y comprueba que el orden y la estructura cumplen las reglas de la gramática independiente del contexto del lenguaje.
Para validar la estructura, construye un árbol de derivación (o árbol sintáctico): - Las hojas son los tokens leídos. - Los nodos internos representan reglas gramaticales aplicadas. - Si el árbol se puede construir completamente hasta la raíz ("programa"), la estructura es correcta.
En la asignatura se trabaja con analizadores ascendentes (bottom-up): se parte de las hojas (tokens) y se va subiendo aplicando reglas hasta llegar a la raíz. ⚠️ EXAMEN
Ejemplo: Para
int x, y, z;el parser construye:int → tipo → clase escalar → clase,x → identificador → lista_id, y verifica queclase + lista_id + ;forma unadeclaraciónválida. Si en lugar de,entre variables hubiera;, el léxico no fallaría (;es un delimitador válido), pero el sintáctico sí: la gramática exige,para separar variables en una misma declaración.
5. Fase 3: Análisis Semántico
El analizador semántico verifica el significado y la coherencia de las construcciones que el sintáctico ha validado estructuralmente. Trabaja sobre el árbol de derivación, "decorándolo" con atributos (gramáticas de atributos). ⚠️ EXAMEN
Comprobaciones que realiza: - Variables declaradas antes de su uso y declaradas una sola vez en el mismo ámbito. - Compatibilidad de tipos en expresiones (ej: no sumar entero con carácter si el lenguaje no lo permite). - Concordancia de tipos en asignaciones (el tipo del resultado debe ser compatible con la variable destino). - Llamadas a funciones: número y tipo de parámetros correctos. - Ámbitos (scopes): gestión de variables en distintos niveles de anidamiento.
El semántico previene errores de ejecución: si pasa el análisis semántico, se minimizan los errores en tiempo de ejecución. ⚠️ EXAMEN
Ejemplo (analogía lingüística): "Lucía corre guapo" es sintácticamente correcto (sujeto + verbo + adjetivo), pero semánticamente incorrecto porque el adjetivo masculino no concuerda con el sujeto femenino. Del mismo modo,
z = x + yes sintácticamente correcto, pero sixes entero eyes un carácter en un lenguaje que no permite esa operación, el semántico lo detectará.Ejemplo (árbol decorado): En
int x, y, z; z = x + y;, el semántico sube por el árbol:int → tipo entero, verifica quex,y,zestán en la tabla de símbolos como enteros, verifica quex + yproduce un entero, y que asignar un entero az(entero) es compatible. Cada nodo del árbol recibe una "bola de Navidad" con la información de tipo.
6. La Tabla de Símbolos
Es la estructura de datos central del compilador, accedida por todas las fases. Almacena para cada identificador: ⚠️ EXAMEN
| Campo | Descripción |
|---|---|
| Nombre | El identificador (x, y, miFunc...) |
| Tipo de identificador | Variable, función, array... |
| Tipo de dato | int, float, char... |
| Ámbito (scope) | Desde qué instrucción hasta cuál es válida |
| Parámetros | (Si es función) número y tipo de argumentos |
| Tamaño | (Si es array) dimensión y tipo de elementos |
7. Generación y Optimización de Código
Tras el análisis, el generador de código traduce el programa a: 1. Código intermedio: instrucciones elementales independientes de la máquina (ej: bytecode de Java). Cada instrucción usa típicamente dos operandos. 2. Optimización: elimina redundancias (valores calculados dos veces, instrucciones innecesarias) para reducir uso de memoria y tiempo. 3. Código máquina/objeto: instrucciones dependientes de la arquitectura (registros, pila, memoria).
Ejemplo: Un
forde alto nivel se descompone en instrucciones elementales: una inicialización, una comparación, un salto condicional y un incremento. De 5 instrucciones de alto nivel pueden surgir 25 instrucciones intermedias.
8. Herramientas de la asignatura
- JFlex: Generador automático de analizadores léxicos. Se le especifican las expresiones regulares y genera el autómata finito correspondiente en Java.
- CUP: Generador automático de analizadores sintácticos/semánticos. Se le especifica la gramática y genera el parser ascendente.
- Eclipse: Entorno de desarrollo recomendado (compatible con JFlex y CUP).
- Lenguaje Pocket: Lenguaje reducido sobre el que se construye el compilador.
9. Prerrequisitos teóricos
Conceptos de Teoría de la Computación recomendados (no imprescindibles si se sigue la asignatura al día): - Autómatas finitos → base del análisis léxico - Expresiones regulares → definen los patrones léxicos - Gramáticas independientes del contexto → definen la sintaxis - Autómatas con pila → reconocen las gramáticas independientes del contexto
10. Bibliografía de referencia
- "Compiladores: Principios, Técnicas y Herramientas" de Aho et al. — Conocido como "el libro del dragón" o "el Aho". Considerado la biblia de los compiladores. Disponible en la biblioteca UNIR.
- "Compiladores e Intérpretes: Teoría y Práctica" (Prentice Hall) — Más reducido y accesible, no disponible en la biblioteca UNIR.
Preguntas de Autoevaluación
- ¿Cuáles son las tres fases del análisis de código en un compilador y qué verifica cada una?
- ¿Cuál es la diferencia fundamental entre un compilador y un intérprete? ¿Y el caso híbrido de Java?
- ¿Por qué solo el analizador léxico interactúa directamente con el código fuente? ¿Qué ventaja aporta esto?
- ¿Qué es un token y qué tipos existen? ¿Por qué el léxico pasa "identificador" al sintáctico en vez del nombre concreto de la variable?
- ¿Qué información almacena la tabla de símbolos y por qué es accedida por todas las fases?
- ¿Qué tipo de error detectaría el análisis semántico que no detectaría el sintáctico? Pon un ejemplo.
- ¿Qué es el bytecode de Java y por qué se considera un código intermedio?
- ¿Por qué los compiladores deben ser deterministas? ¿Qué problemas tendría un compilador no determinista?
- Dado el código
int x, y; x = 5; y = x + 3;, describe paso a paso qué haría cada fase del compilador. - ¿Qué diferencia hay entre un analizador sintáctico ascendente y uno descendente? ¿Cuál se usa en la asignatura?
Guía generada automáticamente a partir de transcripción con faster-whisper + Claude Opus 4.6.