PROF. DR. FRANCISCO J. TORRES-ROJAS Gabriel Vargas Rodríguez 2018103129
Estructura de un compilador (continuación) Estructuras de datos de un compilador
Análisis léxico
Conceptos generales Lenguajes formales
ESTRUCTURA DE UN
COMPILADOR
CONTINUACIÓN ESTRUCTURA
FUENTE TOKENSSCANNER
PARSER
ANÁLISIS SEMÁNTICO GENERACIÓN IR REPRESENTACIÓN INTERMEDIA TABLA DE SÍMBOLOS ÁRBOL DECORADO ÁRBOL SINTÁCTICOLa clase pasada nos habíamos quedado en la generación del código intermedio
EJEMPLO DE CÓDIGO INTERMEDIO
A = B * C + D * E
* C B * E D = + A Pasos:-
Comenzando por las hojas, seagrupan los nodos por operación
-
Creamos “variables” temporales con nombres arbitrarios para agruparcada operación
-
Se van agrupando los nodos hasta llegar a la raíz del árbol-
Recordatorio: la asignación también es una operaciónEJEMPLO DE CÓDIGO INTERMEDIO
A = B * C + D * E
* C B * E D = + AT1 = B * C
T2 = D * E
T3 = T1 + T2
T4 = A = T3
T1
T2
T3
T4
ENTRADA Y SALIDA DE GENERACIÓN I.R.
GENERACIÓN
I.R.
REPRESENTACIÓN INTERMEDIA TABLA DE SÍMBOLOS ÁRBOL DECORADOSCANNER
PARSER
ANALIZADOR
SEMÁNTICO
OPTIMIZADOR DE
FUENTE
FRONT ENDOPTIMIZADOR DE
CÓDIGO
GENERADOR DE
CÓDIGO
BACK END FUENTE OBJETO REPRESENTACIÓN INTERMEDIA Pasamos ahora al generador de códigoEs parte del backend del compilador
Recibe código intermedio y lo convierte en código real
Se puede producir ensamblador (y luego ensamblar el producto) o lenguaje máquina Hay que conocer representación de datos (cantidad de bytes, restricciones de
colocación)
Hay que conocer y explotar las posibilidades de la arquitectura
En este punto no nos preocupamos mucho por generar código bueno y eficiente, solo ocupamos que el código funcione
EJEMPLO GENERACIÓN DE CÓDIGO
T1 = B * C
T2 = D * E
T3 = T1 + T2
T4 = A = T3
mov AX, C mul B mov T1, AX —————— mov AX, E mul D mov T2, Ax —————— mov AX, T2 add AX, T1 mov T3, AX —————— mov AX, T3 mov A, AX mov T4, AXA = B * C + D * E
Recordatorio: en x86, la operaciónmul espera que uno de los
operandos esté en el AX y deja el resultado de la operación en AX
Tip: se pueden tener plantillas de
código ensamblador pre hechas para cada operación
FUENTE TOKENS
SCANNER
PARSER
ANÁLISIS SEMÁNTICO GENERACIÓN IR GENERACIÓN CÓDIGO REPRESENTACIÓN INTERMEDIA TABLA DE SÍMBOLOS LENGUAJE MÁQUINA ÁRBOL DECORADO ÁRBOL SINTÁCTICOSCANNER
PARSER
ANALIZADOR
SEMÁNTICO
OPTIMIZADOR DE
FUENTE
FRONT ENDOPTIMIZADOR DE
CÓDIGO
GENERADOR DE
CÓDIGO
BACK END FUENTE OBJETO REPRESENTACIÓN INTERMEDIA Pasamos ahora al optimizador de códigoMejora el código producido por el generador
Busca instrucciones equivalentes pero más rápidas
Cambiar a modos de direccionamiento más apropiados (ej: es más rápido usar registros que memoria)
Quita código redundante
Elimina código innecesario
Se puede optimizar por espacio o por tiempo
Siempre se puede optimizar más (nunca se van a quedar sin trabajo los que hacen compiladores)
EJEMPLO OPTIMIZADOR
T1 = B * C
T2 = D * E
T3 = T1 + T2
T4 = A = T3
mov AX, C mul B mov T1, AX —————— mov AX, E mul D mov T2, Ax —————— mov AX, T2 add AX, T1 mov T3, AX —————— mov AX, T3 mov A, AX mov T4, AXA = B * C + D * E
mov AX, C mul B mov DX, AX mov AX, D mul E add AX, DX mov A, AX Generación código Generación I.R. OptimizaciónSCANNER
PARSER
ANALIZADOR
SEMÁNTICO
OPTIMIZADOR DE
FUENTE
FRONT ENDOPTIMIZADOR DE
CÓDIGO
GENERADOR DE
CÓDIGO
BACK END FUENTE OBJETO REPRESENTACIÓN INTERMEDIA¡Listo! Pero… ¿en realidad es así?
FUENTE TOKENS
SCANNER
PARSER
ANÁLISIS SEMÁNTICO GENERACIÓN IR GENERACIÓN CÓDIGO REPRESENTACIÓN INTERMEDIA TABLA DE SÍMBOLOS LENGUAJE MÁQUINA ÁRBOL DECORADO ÁRBOL SINTÁCTICO LENGUAJE MÁQUINA MEJORADO OPTIMIZACIÓNEsto es una abstracción útil para comprender qué
pasa, pero en realidad el compilador no es
ESTRUCTURAS DE DATOS
DE UN COMPILADOR
Estructura con dos campos (usualmente):
Código de token: número que indica a qué categoría de token pertenece
BEGIN
INTLITERAL RPAREN
Hilera específica dentro del fuente (llamado lexema) valor particular
nombre de la variable… Los regresa el scanner
TOKEN
Lo crea el parser
Luego, es decorado por el analizador semántico Árbol creado en memoria dinámica
En el compilador de micro, nunca se crea una
estructura “árbol”. Este está representado en las funciones entre ellas (system goal, program,
statement_list…)
Hay una variable que señala a la raíz
Los campos son llenados por el parser y el analizador semántico
Los campos podrían tener punteros a otras estructuras (ej: a la tabla de símbolos, tabla de constantes…)
A veces (como en el compilador de Micro), no existe
Contiene la información asociada a todos los identificadores (de variables, de funciones, de
constantes… cualquier cosa que tenga un nombre) Casi todas las partes del compilador interactúan
con la tabla de símbolos (scanner, parser, analizador semántico…)
Con los profilers, se ha determinado que es la
estructura de datos más usada por un compilador Dado que se usa tanto, es buena idea
implementarla con hash
Puede ser formada por varias tablas, no solo una
Contiene todas las constantes (numéricas e hileras)
Como contiene valores constantes, esta tabla no cambia Está en memoria
Sus campos pueden ser apuntados por la tabla de símbolos
Ayuda a reducir el tamaño final del código (tal vez no más rápido, pero sí más pequeño)
Una de las banderas típicas del gcc es para indicar si se quiere optimizar por tiempo o tamaño
Se puede representar de muchísimas formas: Arreglo de punteros a hileras
Archivo de texto
Lista enlazada de hileras…
El tipo de representación depende de lo que quiera hacer con él
El proceso de optimización puede necesitar reorganizar el código intermedio, entonces
necesita poder borrar, reorganizar, reemplazar… una lista enlazada es más apropiado que un archivo para hacer esto, por ejemplo
Muy utilizado por los compiladores
Vida de un archivo temporal: Crea archivo,
graba cosas, lo cierra, lo lee después, lo cierra y lo borra
Los archivos temporales viven solo por unos cuantos segundos
A veces el sistema operativo ayuda al
compilador, no llegando a guardar los archivos en disco
Muchos problemas complicados son fáciles de resolver con archivos temporales
ARCHIVOS TEMPORALES
Sistema operativo
Archivo Temporal
Léxico = vocabulario
Se le conoce como scanner
Toma el fuente y lo va descomponiendo en categorías léxicas mínimas: tokens
El scanner busca el símbolo más grande que puede reportar
Los tokens del lenguaje natural son las palabras
Es lo primero que se aprende cuando se estudia un idioma
EJEMPLO DE ANÁLISIS LÉXICO
void bubbleSort(int arr[], int n)
{
int i, j, tmp;
for (i = 0; i < n-1; i++)
for (j = 0; j < n-i-1; j++)
if (arr[j] > arr[j+1]) {
tmp = arr[j];
arr[j] = arr[i];
arr[i] = tmp;
}
}
Palabras reservadas IDs Operadores Constantes PuntuaciónVERSIÓN ABSTRACTA: MENTIRA
Qué cuento más mentira
FUENTE TOKENS
Lo más usual en los compiladores
Todo el proceso dirigido por el parser
Invoca al scanner cada vez que necesita un token (parser es cliente del scanner)
Token getToken(void);
Invoca a las rutinas semánticas cuando corresponde
El trabajo del parser termina cuando se generó la representación intermedia o incluso el lenguaje máquina
TRADUCCIÓN DIRIGIDA POR SINTAXIS
El proceso no es tan lineal como
parecía en la versión abstracta
PARSER
SCANNER
SEMÁNTICAS
RUTINAS
Producto
GetToken()
Regresa el siguiente token
Un token representa a un conjunto de hileras equivalentes sintácticamente (diferentes léxica y semánticamente)
Podría ser:
Un solo elemento: LPAREN
Una cantidad pequeña de elementos: int, float, char… Conjunto potencialmente infinito: números, strings
GetToken()
PARSER
SCANNER
Estructura del compilador con dos campos: Tipo de token
LPAREN
INTLITERAL ID
Puntero al lexema (hilera particular) “12.34”
“foo”
NULL (a veces no se necesita especificar un lexema, como en el caso de LPAREN o COMMA, pues el lexema es el mismo siempre)
Un token representa una categoría léxica
TOKEN
Categoría
Puntero al lexema
Token: categoría léxica, nombre del conjunto
Lexema: hilera particular en el programa que corresponde al token reportado por el scanner
Un token contiene muchos lexemas
Un lexema puede corresponder solo a un token
La estructura Token tiene dos campos, el nombre de la categoría (token) y el lexema particular
El programa completo es una hilera en memoria
Scanner mantiene una posición actual conforme va leyendo Se salta “espacios” (blancos, salto de línea, tabs, ….)
Avanza hasta reconocer un token completo (símbolo más grande que pueda reportar) Regresa token
GetToken() EN EJECUCIÓN
f o o = 4 * 2 salta tab reconoce el símbolo más grande posibletermina cuando reconoce el fin del token
Guarda posición actual ID
Puntero a “foo”
Generalmente, no hay un token que represente espacio en blanco (hay excepciones como Python)
Hay que ignorarlos (en algún momento) Para Fortran, Backus propuso eliminarlos todos antes de compilar
DO: se usa para hacer ciclos sobre una serie de instrucciones
Altera una variable dado un valor
inicial, un valor final y un incremento, sumando el incremento al valor
inicial (en cada iteración) hasta llegar al valor final
EJEMPLO DO FORTRAN
Fortran: j = 0 DO 10 i = 1,10,1 j = j + i 10 CONTINUE C: int j = 0;for (int i=1;i≤10;i++) {
j = j + i; }
El compilador elimina previamente los espacios en blanco
Debe avanzar hasta encontrar un token
No puede parar solo en DO, porque el los nombres de variables pueden contener números
Como no sabe si se trata de DO o de DO10i, el
scanner continúa hasta eliminar ambigüedad y toma DO10i
El scanner no se da cuenta que está lidiando con un DO hasta que encuentra la primera coma
Debe regresarse 6 tokens (perdió tiempo)
SCANNER Y DO DE FORTRAN
D O 1 0 i = 1 , 1 0 , 1
Los espacios no significan nada en el código, pero son buenos separadores: son
nuestros amigos
No es buena idea eliminar los espacios antes de
compilar
La teoría de scanners ya es muy conocida (estudiada principalmente en los 50)
Hay mucha teoría y experiencia acumulada respecto a análisis léxico
Casi todo se puede automatizar
lex, flex: generadores de scanners (yacc y bison generan parsers)
Generar un scanner es relativamente fácil
Siempre hay que agregar un poco de código a mano
La función principal del scanner es detectar tokens
Avanzar en la entrada hasta reconocer el token más grande posible
Para hacer esto, nos apoyamos en algunas de las siguientes técnicas:
lenguajes formales teoría de autómatas
autómatas determinísticos de estados finitos
Definición
Habilidad humana para adquirir y usar complejos sistemas de comunicación
El estudio de los lenguajes corresponde a la lingüística
Conjunto de sonidos o señales que manifiestan lo que se piensa o se siente: expresan algo
Tipos
Lenguajes naturales (español, inglés, alemán…)
Lenguajes de computadora (C, Java, Ensamblador…) Lenguaje matemático
Lenguajes formales (este es el que nos interesa ahorita)
Concepto primitivo (no lo definimos, sino que lo aceptamos así como es)
“Representación” de un concepto, idea o entidad, con un significado convencional
No hay nada intrínseco en el símbolo, sino que decidimos qué iban a significar
No necesariamente es un solo carácter (o es lo mismo que un carácter)
Denotados como las primeras letras del alfabeto en itálica (a, b, c)
Definición: conjunto finito y no vacío de símbolos Recordatorio: los conjuntos no tienen orden y no tienen elementos repetidos
Usualmente denotado como ∑ Ejemplos
∑ = {a, b, c, …, x, y, z}
∑ = {0,1,2,3,4,5,6,7,8,9} ∑ = {0, 1}
∑ = {A,T,C,G}
∑ = {Ala, Arg, Asn, …, Trp, Tyr, Val} -> aminoácidos
Secuencia de longitud arbitraria formada con símbolos tomados de un alfabeto ∑ Diferencia entre conjunto y secuencia:
la secuencia puede tener símbolos repetidos la secuencia tiene orden
Denotadas como w, x, y, z (generalmente)
También se les conoce como “palabras” o “frases”
Sea ∑ = {0,1} 1010101 1
000
1010100101111100101
EJEMPLOS HILERA SOBRE ∑
Sea ∑ = {A,T,C,G} ACATGA
GGGC
Sea w una hilera sobre ∑
|w| = longitud de la hilera w = cantidad de símbolos de w Ejemplos
|CACTAGACTACAG| = 13 |01| = 2
|GUAUAUAUAUAUAUA| = 15
La longitud puede ser 0 -> hilera vacía
Hilera vacía se representa como Ɛ (forma más tradicional) o λ | Ɛ | = 0
Sea w una hilera sobre ∑
Un prefijo de w es una hilera formada tomando (en el mismo orden) los primeros k símbolos (de la izquierda) de w
0 ≤ k ≤ |w|
si k < |w|, tenemos un prefijo propio
si k = |w|, tenemos un prefijo no propio
Como k puede ser 0, Ɛ es prefijo de cualquier hilera (prefijo vacío)
Sea ∑ = {A,T,C,G} y w = ACTCGCTAAGC una hilera sobre ∑ Los siguientes son prefijos de w
ACTCG A
ACTCGCTAAGC <- prefijo no propio Ɛ
Sea w una hilera sobre ∑
Un sufijo de w es una hilera formada tomando (en el mismo orden) los últimos k símbolos (de la derecha) de w
0 ≤ k ≤ |w|
si k < |w|, tenemos un sufijo propio
si k = |w|, tenemos un sufijo no propio
Como k puede ser 0, Ɛ es sufijo de cualquier hilera (sufijo vacío)
Sea ∑ = {A,T,C,G} y w = ACTCGCTAAGC una hilera sobre ∑ Los siguientes son sufijos de w
TAAGC C
ACTCGCTAAGC <- sufijo no propio Ɛ
Una subhilera de w es una hilera formada tomando (en el mismo orden) k símbolos consecutivos de w a partir de una posición j
0 ≤ k,j ≤ |w| 0 ≤ k + j ≤ |w|
Si k + j < |w| y j > 0, tenemos una subhilera propia
Como k puede ser 0, Ɛ es subhilera de cualquier hilera (subhilera vacía)
Sea ∑ = {A,T,C,G} y w = ACTCGCTAAGC una hilera sobre ∑ Las siguientes son subhileras de w
TGCG
GC <- también es un sufijo, pero es una subhilera no propia ACTCGCTAAGC <- subhilera no propia
Ɛ
Sean v y w dos hileras sobre ∑
Si v = a1a2a3a4…an y w = b1b2b3b4…bk entonces:
La concatenación de v y w es la hilera: a1a2a3a4…
anb1b2b3b4…bk
Esto se denota como vw
La concatenación consiste en escribir una hilera justo después de la otra
Sea ∑ = {01} Sean
x = 01 y = 10
z = 10101
EJEMPLOS CONCATENACIÓN DE HILERAS
Tenemos que xy = 0110
yz = 1010101
Asociativa
xyz = (xy)z = x(yz)
No es conmutativa (generalmente)
wv ≠ vw
Las longitudes se suman |vw| = |v| + |w|
Elemento neutro = Ɛ
vƐ = Ɛv = v
Sea v una hilera sobre ∑
La potencia n-esima de v es la hilera resultado de concatenar n copias de v Se denota como vn, con n = cantidad de veces que se concatena
Casos especiales
v1 = v
v0 = Ɛ <- porque no concatenamos v ni una sola vez
Sea v = ATCGA
v1 = ATCGA
v2 = ATCGAATCGA
v10 = ATCGAATCGAATCGAATCGAATCGAATCGAATCGAATCGAATCGAATCGA
v0 = Ɛ
Sea ∑ un alfabeto, entonces ∑k es el conjunto de todas las hileras sobre ∑ tales que tengan una longitud de k Sea ∑ = {0,1} ∑3 = {000, 001, 010, 011 100, 101, 110, 111} ∑2 = {00,01,10,11} ∑1 = {0,1} ∑0 = {Ɛ}
∑ ≠ ∑1 porque ∑ es un conjunto de símbolos y ∑1 es un conjunto de hileras
Sea ∑ un alfabeto, el conjunto ∑* se define recursivamente como:
1. 1. Ɛ ∈ ∑*
2. Si w ∈ ∑* y a ∈ ∑*, entonces wa ∈ ∑*
3. w ∈ ∑* solo si puede ser construida desde Ɛ usando el paso 2
repetidamente Equivalentemente
Σ* =
⋃
k i=0Σ
iCONJUNTO ∑*
Todas las posibles hileras que se pueden hacer con el alfabeto ∑
Es un conjunto infinito, pero ninguno de sus miembros (hileras) tiene longitud infinita
Las hileras pueden ser arbitrariamente largas, pero no infinitas