Skip to content

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:

  1. Leer el código carácter a carácter y agrupar en palabras/elementos.
  2. Clasificar cada elemento en su tipo de token: identificador, palabra reservada, constante numérica, operador, delimitador.
  3. Eliminar comentarios, espacios en blanco, tabuladores y saltos de línea.
  4. Detectar errores léxicos: símbolos no permitidos (ej: $), identificadores mal formados (ej: empezar por número), constantes mal construidas, comentarios mal cerrados.
  5. 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 que clase + lista_id + ; forma una declaración vá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 + y es sintácticamente correcto, pero si x es entero e y es 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 que x, y, z están en la tabla de símbolos como enteros, verifica que x + y produce un entero, y que asignar un entero a z (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 for de 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

  1. ¿Cuáles son las tres fases del análisis de código en un compilador y qué verifica cada una?
  2. ¿Cuál es la diferencia fundamental entre un compilador y un intérprete? ¿Y el caso híbrido de Java?
  3. ¿Por qué solo el analizador léxico interactúa directamente con el código fuente? ¿Qué ventaja aporta esto?
  4. ¿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?
  5. ¿Qué información almacena la tabla de símbolos y por qué es accedida por todas las fases?
  6. ¿Qué tipo de error detectaría el análisis semántico que no detectaría el sintáctico? Pon un ejemplo.
  7. ¿Qué es el bytecode de Java y por qué se considera un código intermedio?
  8. ¿Por qué los compiladores deben ser deterministas? ¿Qué problemas tendría un compilador no determinista?
  9. Dado el código int x, y; x = 5; y = x + 3;, describe paso a paso qué haría cada fase del compilador.
  10. ¿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.