Você está na página 1de 83

CURSO: Lenguajes y

Autómatas II
Dr. Ramón Zatarain Cabada
Repaso
¿Qué es y que estudia la P. de S. ?

 Son los programas que residen en un


sistema de computación. Su función es
proporcionar al usuario o programador una
interfase mas eficiente y practica con relación
al hardware de la maquina.
La P. de S. estudia como están
implementados cada uno de los programas
de un Sistema (ver notas).
Herramientas desarrolladas con
la P de S
Ejemplos:
 Compiladores (javac)
 Ensambladores (Masm)
 Interpretes (Visual Basic)
 Ligadores (Link)
 Cargadores
 Sistema Operativo (Windows)
 Utilerías de Sistemas (Debugger)
Lenguajes
 Naturales (Traductores de Ingles-Español,
Ingles-Ruso, etc)
 Artificiales (Compiladores de LP como Java,
C++, Ada, etc.)
Traductor y su Estructura

 Ensambladores …. (notas)

 Compiladores……. (notas)

 Intérpretes……...... (notas)
Generador de Código para Compiladores
(Compilador de Compiladores)

 Definición: Un compilador de compiladores o generador de


Parsers es una utilería para generar el código fuente de un
parser, intérprete o compilador a partir de una descripción de
lenguaje anotado en la forma de gramática (usualmente BNF)
mas código que es asociado con cada una de las reglas de la
gramática que debe ser ejecutada cuándo esas reglas sean
aplicadas por el parser. Esas piezas de código son algunas
veces llamadas rutinas de acciones semánticas ya que ellas
definen la semántica de las estructura sintáctica y que es
analizada por el parser. Dependiendo del tipo de parser que será
generado, las rutinas pueden construir un árbol de parsing (o
AST) o generar código ejecutable directamente (notas).
Introducción a las gramáticas
Libres de Contexto y Árboles de
derivación

 El tipo de gramáticas usadas en los LP son llamadas gramáticas de contexto libre, las cuáles,
junto con árboles de derivación, fueron estudiadas en el curso de Lenguajes y Autómatas.

 Un ejemplo de una gramática para un LP simple es la siguiente:

1) SS;S 2) Sid := E 3) Sprint (L)


4) E id 5) E num 6) E E + E 7) E(S,E)
8) L E 9) L L , E

 (ver ejemplo de derivaciones y árboles de parsing)

 A continuación veremos un ejemplo de una gramática para Java en formato BNF (liga).
(cont.)
 Gramática Ambigua. Una gramática es ambigua si
puede derivar una oración (cadena) con dos
diferentes árboles de parsing. La sig. Gramática es
ambigüa:

Eid Enum EE*E EE/E


EE+E EE-E E(E)

ya que tiene dos árboles de parsing para la misma


oración (árboles para id:=id+id+id con primera
gramática y árboles para 1-2-3 con segunda en sig
“slice”).
(cont.)
S
S
Id := E
Id := E
E + E
E + E
E + E id
id E + E
Id id
id id
(cont.)

E
E
E - E
E - E
E - E 3
1 E - E
1 2
2 3
Analizador Sintáctico
 En esta fase se analiza la estructura de la frase del programa.
 El parser es el programa que funciona como núcleo del compilador.
Alrededor del parser funcionan los demás programas como el scanner, el
analizador semántico y el generador de código intermedio. De hecho se
podría decir que el parser comienza el proceso de compilación y su primer
tarea es pedir al escáner que envíe los tokens necesarios para llevar a
cabo el análisis sintáctico, del estatuto, expresión o declaración dentro de
un programa.
 También el parser llama rutinas del análisis semántico para revisar si el
significado del programa es el correcto.
 Por ultimo el parser genera código intermedio para los estatutos y
expresiones si no se encontraron errores en ellos.
(cont.)
Existen diferentes técnicas o métodos para realizar un análisis
sintáctico “Parsing”. Estas técnicas se dividen en dos tipos:

 Descendentes
 Ascendentes

Las técnicas descendentes realizan su análisis partiendo desde el


símbolo inicial de la gramática y realizando derivaciones hasta
llegar a producir las hojas o tokens.
Por otra parte las técnicas ascendentes realizan su análisis
partiendo de las hojas o tokens y mediante una serie de
operaciones llamadas reducciones terminan en la raíz o símbolo
inicial de la gramática.
Por lo general las técnicas descendentes son mas sencillas de
implementar que las ascendentes, pero por otra parte son menos
eficientes
Analizador descendente (LL).

Parsing Predictivo
 Algunas gramáticas son sencillas de analizarse sintácticamente
usando un algoritmo o método cuyo nombre es recursivo
descendente. En esta técnica cada producción de la gramática
se convierte en una cláusula de una función recursiva.
 Un parser Predictivo es aquel que “ predice ” que camino tomar
al estar realizando el análisis sintáctico. Esto quiere decir que el
parser nunca regresara a tomar otra decisión ( back tracking )
para irse por otro camino, como sucede en las derivaciones.
Para poder contar con un parser predictivo la gramática debe de
tener ciertas características, entre ellas la mas importante es que
el primer símbolo de la parte derecha de cada producción le
proporcione suficiente información al parser para que este
escoja cual producción usar. Normalmente el primer símbolo
mencionado es un Terminal o token.
(cont.)
 Esta técnica se utilizó o popularizó en los años 70 a partir del
primer compilador de pascal implementado con ella. A
continuación ilustraremos esto escribiendo un parser recursivo
descendente para la siguiente gramática:

S  if E then S else S
S begin S L
S print E
L end
L ; S L
E num = num

Nuestro Parser tendría 3 métodos (uno por cada producción)


Programa del Parser
Final int if = 1, then = 2, else = 3, begin = 4, end = 5, print = 6, semi = 7,
num = 8, EQ = 9

int tok = get token ( );

void advance ( ) { tok = get token ( ); }


void eat ( int t) { if ( tok == 1) advance ( ); else error ( ); }

void S ( ) { switch ( tok ) {


case If: eat ( if ); E ( ); eat ( then ); S ( );
eat ( else ); S ( ); break;
case begin: eat ( begin ); S ( ); L ( ); break;
case print: eat ( print ); E ( ); break;
default: error;
}}
void L ( ) { switch ( tok ) {
case end: eat ( end ); break;
case semi: eat ( semi ); S ( ); L ( ); break;
default: error ( );
}}
void E ( ) { eat ( num ); eat ( EQ ); eat ( num ); }
(cont.)
 Un parser predictivo que examina la entrada de izquierda a
derecha (left-to-right) en un paso y realiza su derivación por la
izquierda es llamado “Parser LL”.
 Cuando el parser solo necesita “mirar” el siguiente token para
hacer su función (llokahead(1)), recibe el nombre de Parser
LL(1).
 Un parser podría necesitar “mirar” K tokens por adelantado para
tomar desiciones. En este caso recibe el nombre de parser
LL(K).
Ejemplo: (parte de una gramática)

IF-STM  if EXP then STM else STM


|  if EXP then STM
Eliminación de Recursión por la izquierda

 Suponer que queremos construír un Parser


predictivo para la gramática de sección 4.3.
SE$ EE+T EE-T ET
T-->T*F TT/F TF
Fid Fnum F(E)

Producciones como E  E + T contienen recursión por la izquierda.


Parsers descendentes no pueden manejar recursión por la izquierda
en una gramática. Para eliminar este tipo de recursión utilizamos la
siguiente transformación:
(cont.)
En general, si tenemos producciones X  X g
y X  a, donde a no comience con X
podemos aplicar la siguiente transformación:

X  Xg1 X  a1X’
X  Xg2 X  a2X’
X  a1 X’  g1X’
X  a2 X’  g2X’
X’  l
(cont.)
 Aplicando la transformación a la gramática anterior,
obtenemos la nueva equivalente gramática (sin
recursión por la izquierda):
S  E$
E  T E’ E’  + T E’ E’  - T E’ E’  l
T  F T’ T’  * F T’ T’  / F T’ T’  l
F  id F  num F  (E)
Factorización por la izquierda
 Otro problema en una gramática ocurre cuando dos producciones
para el mismo no terminal comienza con el mismo símbolo. Por
ejemplo:
IF-STM  if EXP then STM else STM
IF-STM  if EXP then STM

En dicho caso podemos factorizar por la izquierda la gramática. El


resultado sería el sig:
IF-STM  if EXP then STM X
X l
X  else IF-STM

las producciones anteriores facilitarán el trabajo del parser


predictivo.
Unidad I
Análisis de Semántica
Analizador semántico

 Un compilador no solo tiene que revisar la sintaxis de


código fuente, si no también la semántica de este.
 Al igual que en los lenguajes naturales (español, ingles,
etc.) en los lenguajes de programación existen reglas
semánticas para definir el significado de los programas,
estatutos, expresiones, etc.
 Por ejemplo un error semántico es usar (en pascal ó
java) un identificador que no fue anteriormente
declarado.
 Otro ejemplo de error semántico en un programa es
cuando este es compilado y y no se detectan errores
pero el momento de ser ejecutado este programa no
funciona correctamente.
Verificación de tipos en expresiones.

 Cuando mezclamos diferentes tipos en una


misma expresión o que llamamos una rutina
que no existe existe un error semántico.
 Una de las funciones del analizador smántico
es verificar que los tipos de una expresión
sean compatibles entre si.
 Para hacer lo anterior el compilador cuenta
con información de los atributos (tipos,
tamaño, número de argumento, etc.) de los
identificadores en una tabla de símbolos.
Conversión de tipos.

 Algunas veces los tipos de una expresión o estatuto son


diferente.
 Por ejemplo en la asignación,
a = b * c;
el tipo del resultado de evaluar b*c es diferente al de el
identificador a.
 El compilador algunas veces con ciertos diferentes tipos puede
hacer una conversión interna en forma implícita para solucionar
el problema. Otras veces el programador explícitamente es el
que hace la conversión (casting).
Ejemplo:
float dinero;
int cambio;
dinero = (float) cambio;
Acciones agregadas en un Parser
Recursivo-descendente
 En un parser recursivo-descendente, las acciones
semánticas son los valores retornados por las
funciones de parsing, o los efectos laterales de
esas funciones o ambos.
 Por cada símbolo terminal y noterminal, asociamos
un tipo (desde el lenguaje de implementación del LP
del compilador) de valor semántico representando
frases derivadas desde ese símbolo.
 El siguiente programa es un intérprete recursivo
descendente para una parte de la gramática en la
cual eliminamos la recursión por la izquierda (por
conveniencia la volvemos a mostrar):
(cont.)

S  E$ E  T E’ E’  + T E’ E’  - T E’ E’  l
T  F T’ T’  * F T’ T’  / F T’ T’  l
F  id F  num F  (E)

Los tokens ID y NUM deben ahora acarrear valores de tipo string e int,
respectivamente. Asumiremos que existe una tabla “lookup” que mapea
identificadores a enteros. El tipo asociado con E, T, F, etc., es int, y la acción
semántica es fácil de implementar.
Interprete Acciones semánticas

class Token2 {
int kind; Object val;
Token2(int k, Object v) int T() {switch(tok.kind) {
{ case ID:
kind=k; case NUM:
val=v; case LPAREN: return Tprime(F());
} default: print("2 esperaba ID, NUM o parent izq");
} //skipto(T_follow);
final int EOF=0, ID=1, NUM=2, PLUS=3, return 0;
MINUS=4,LPAREN=5, RPAREN=6, TIMES=7; }}
int lookup(String id) { …. }
int F_follow[] = {PLUS,TIMES,RPAREN,EOF}; int Tprime(int a) {switch (tok.kind) {
case TIMES: eat(TIMES); return Tprime(a*F());
int F() {switch(tok.kind) { case PLUS:
case ID: int i=lookup((String)(tok.val));advance()return i; case RPAREN:
case NUM: int i=((integer)(tok.val)).intVal(); case EOF: return a;
advance();return i; default: print("3 esperaba ID, NUM o parent izq");
case LPAREN: eat(LPAREN); //skipto(T_follow);
int i=E(); return 0;
eatOrSkipTo(RPAREN,F_follow); }}
return i;
case EOF: void eatOrSkipTo(int expected, int[] stop) {
default: print("1 esperaba ID,NUM, o parent izq"); if (tok.kind==expected)
//skipto(F_follow); return 0; eat(expected);
}} else {print("4 esperaba ID, NUM o parent izq");
//skipto(stop);}
int T_follow[]= {PLUS,RPAREN,EOF}; }
Árboles de Parsing Abstractos
 Para mejorar la modularidad del compilador, es recomendable separar detalles de la
sintaxis con detalles de la semántica (chequeo de tipos y traducción a código
máquina).
 Una forma de hacerlo es producir un árbol de sintaxis abstracta (una forma
condensada de árbol de parsing útil para representar construcciones del LP).
 Por ejemplo la producción S  if B then S1 else S2 pudiera aparecer en un arbol
sintáctico como:

Árbol
Est-if
Árbol If-then-else De
sintáctico parsing

If Exp ( Exp ) Est

B S1 S2

En un árbol sintáctico, los operadores y las palabras claves (reservadas)


no aparecen como hojas, sino que están asociadas con el nodo interior
que sería el padre de esas hojas en el arbol de parsing.
(cont.)
 Otro ejemplo es en el árbol de parsing:
L

E + T

T * F F

F 4 8

2 +
Cuyo árbol sintáctico abstracto sería: * 8

2 4
Ejemplo:
 La gramática siguiente nos muestra una sintaxis
abstracta de un lenguaje para expresiones:
EE+E EE-E EE*E
EE/E Eid Enum
 Esta gramática es impráctica para un parser ya que
es ambigua pues no tiene precedencia de
operadores.
 Sin embargo, esta gramática no es para el parser.
El analizador semántico podría usarla el cual no se
molesta por la ambiguedad puesto que ya tiene su
arbol.
Administración de la tabla de
símbolos

 El análisis semántico conecta las definiciones de las


variables con sus usos, checa que cada expresión
tenga un tipo correcto y traduce la sintaxis abstracta
a una representación mas simple para generar
código máquina.
 Esta fase es caracterizada por el mantener la tabla
de símbolos (también llamada “environment”) la cual
mapea identificadores con sus tipos y localidades.
 Cada variable local en un programa tiene un ámbito
(scope) dentro del cual es visible. Por ejemplo, en
un método MiniJava m, todos los parámetros
formales y variables locales declarados en m son
visibles solo hasta que finalice m.
Chequeo de Tipos en MiniJava
 ¿Con que se llena una tabla de símbolos? Esto es,
¿Qué es la ligadura o “binding”?
 Para realizar el chequeo de tipos de programas
MiniJava (subconjunto de Java), la tabla de
símbolos debe contener toda la información
declarada:
 Cada nombre de variable y nombre de parámetro formal
debe ser ligado a su tipo.
 Cada nombre de método debe ser ligado a sus
parámetros, tipo de resultado y variables locales.
 Cada nombre de clase debe ser ligado a su variable y
declaraciones de métodos.
 Liga a página con mas información.
(cont.)
 Por ejemplo, considere la siguiente figura,
que muestra un programa y su tabla de
símbolos. PARAMS
p int
Class B {
q int
C f; int [ ] j; int q;
public int start(int p, int q) { FIELDS
int ret; int a; f C LOCALS
/* …… */ j int [ ] ret int
return ret; q int a int
B
} C METHODS
public boolean stop(int p) { start int
/* ….. */ stop bool PARAMS
return false;
p int
}
} …… LOCALS
Class C {
/* ….*/
}
(cont.)
 Los tipos primitivos en MiniJava son int y boolean;
todos los otros tipos son arreglo de enteros o
nombres de clases.
 Por simplicidad todos los tipos son “string”, en lugar
de símbolos; esto nos permite checar igualdad de
tipos por medio de comparación de “strings”.
 El chequeo de tipos de un programa MiniJava
ocurre en dos fases. Primero, construimos la tabla
de símbolos y después checamos los tipos de los
estatutos y las expresiones. Lo hacemos en dos
fases porque en MiniJava (igual que en Java) las
clases son mutuamente recursivas.
Manejo de errores semánticos.

 Cuando el checador de tipos detecta un error de


tipos o un identificador no declarado, debe imprimir
el mensaje de error y continuar.
 Esto debido a que normalmente el programador
prefiere que le describan todos los errores posibles
del programa fuente.
 Esto quiere decir, que si un error de tipos es
encontrado, no debe producirse un programa objeto
por parte del compilador.
 Así, las siguientes fases no deben ejecutarse.
 Hasta esta etapa (chequeo de tipos), la parte del
compilador se conoce con el nombre de “front End”.
Unidad II
Generación de Código
Intermedio
Lenguajes intermedios.

 El código intermedio en una estructura de código


cuya complejidad está entre un código fuente en un
lenguaje de alto nivel y el código máquina.

Código fuente  Código intermedio  Código Objeto

 Un compilador produce primero un código


intermedio, pues producir directamente el código
objeto resulta sumamente complicado e ineficiente.
(cont.)
Ventajas de producir código intermedio:
 Más fácil de producir código objeto después,
si se parte de un código intermedio.
 Facilita y hace eficiente la optimización de
código (algunos tipos de optimización).
 El código intermedio es transportable (puede
usarse en diferentes arquitecturas) ya que no
hace referencia a componentes de la
máquina.
Notaciones.

 Infija. Es la notación habitual. El orden es primer


operando, operador, segundo operando.
Ejemplo: a/b/c
La notación infija tiene el problema de que en
expresiones con más de un operador existe
ambiguedad sobre cual es el orden de evaluación.
Por ejemplo, la expresión 8/4/2 se puede interpretar
como (8/4)/2 o bien como 8/(4/2). Las otras
notaciones (prefija y postfija) no sufren este
problema.
(cont.)
 Postfija. El orden es primer operando, segundo operando, operador.
Ejemplo: La expresión X+Y-X*Y
en notación Postfija es XY+XY*-
Por lo general la notación postfija se emplea en máquinas de pilas ya que la pila facilita la
ejecución. Al analizar una notación postfija de izquierda a derecha, cada vez que se detecta un
operando se mete a la pila. La ocurrencia de un operador con ‘m' operandos significa que el
enésimo operando estará m-n posiciones por debajo del tope de la pila. Después se sacan los
operandos de la pila y se mete el resultado de la operación.
Por ejemplo, suponer que X=1 y Y=2.

Las operaciones serían:


push 1 (meter 1)
push 2
' + ' requiere de 2 operandos, se sacan, se suman y se mete el resultado (3)
push 1
push 2
' * ' se mete el resultado(2)
' - ' se mete el resultado (1)

 Notación prefija: El orden es operador, primer operando, segundo operando.


Representación de código
intermedio.

 Existen muchas clases de representaciones


intermedias. Algunas de las mas comunes
son:
 Notación polaca.
 Código P (Pascal).
 Triples y Cuadruples.
 Bytecodes (Java)
 MSIL (C#)
Notación Polaca
 También conocida como notación postfija. Se
utiliza como se dijo anteriormente en
máquinas de Pila.
Ejemplos:
Pascal Notación Polaca
a+b-c ab+c-
a+b*c abc*+
a+b*c+d abc*+d+
Código P
 Se usó como código intermedio y objeto en las
primeras implementaciones de Pascal.
 El código P era interpretado en una máquina
abstracta.
 Algunas implementaciones de Basic y Pascal usan
código P el cual después es traducido a código
nativo por un compilador “Just-in-Time”.
 La máquina P está orientada a usarse en una pila
(stack-oriented).
Ejemplo:
Insn. Stack Stack Description
before after
adi i1 i2 i1+i2 add two integers
Adr r1 r2 r1+r2 add two reals
dvi i1 i2 i1/i2 integer division
ldci i1 i1 load integer constant
mov a1 a2 move
not b1 ~b1 boolean negation
Triples y Cuadruplos
(Código de 3 Direcciones)
 Ha sido uno de los mas populares. Sus
instrucciones constan de 3 direcciones o registros
para 2 argumentos u operandos y el resultado.
Su formato es:
resultado:= argumento1 operador argumento2
Donde resultado, argumento1 y argumento2 pueden
ser constantes, identificadores y variables
temporales definidos por el compilador mientras que
operador representa una operación arbitraria. Esta
forma se denomina Cuádruplo.
(cont.)

EJEMPLO: Z:= X + Y – X * Y

ADD X Y VAR1
MUL X Y VAR2
SUB VAR1 VAR2 VAR3
STORE VAR3 Z
(cont.)

 EJEMPLO: (Estructure de Control):


If (a==b)
a=0;
else
a=1;
 En cuadruplos tendríamos:
1 - A B t1
2 JnZ t1 5
3 = 0 A
4 JP 6
5 = 1 A
6
(cont.)

 En el código de 2 direcciones se evita el uso se


variables temporales. El formato es:
Operador argumento1 argumento2
EJEMPLO: Z = X + Y - X * Y
1. ADD X Y
2. MUL X Y
3. SUB (1) (2)
4. STORE (3) (Z)

Donde los números entre paréntesis representan


apuntadores a la secuencia de operaciones de 2 direcciones.
Java Bytecodes

 El bytecode es un código intermedio más abstracto que el código


máquina. Habitualmente viene a ser un archivo binario que
contiene un programa ejecutable similar a un módulo objeto o
código máquina producido por el compilador.
 El bytecode recibe su nombre porque generalmente cada código
de operación tiene una longitud de un byte, si bien la longitud del
código de las instrucciones varía.
 Cada instrucción tiene un código de operación entre 0 y 255
seguido de parámetros tales como los registros o las direcciones
de memoria. Esta sería la descripción de un caso típico, si bien
la especificación del bytecode depende ampliamente del
lenguaje.
(cont.)

 Como código intermedio, se trata de una forma de salida


utilizada por los implementadores de lenguajes para reducir la
dependencia respecto del hardware específico y facilitar la
interpretación.
 Menos frecuentemente se utiliza el bytecode como código
intermedio en un compilador. Algunos sistemas, llamados
traductores dinámicos o compiladores just-in-time (JIT) traducen
el bytecode a código máquina inmediatamente antes de su
ejecución para mejorar la velocidad de ejecución.
 Los programas en bytecode suelen ser interpretados por un
intérprete de bytecode (en general llamado máquina virtual, dado
que es análogo a un ordenador).
(cont.)

 Su ventaja es su portabilidad: el mismo código binario puede ser


ejecutado en diferentes plataformas y arquitecturas. Es la misma
ventaja que presentan los lenguajes interpretados.
 Sin embargo, como el bytecode es en general menos abstracto,
más compacto y más orientado a la máquina que un programa
pensado para su modificación por humanos, su rendimiento
suele ser mejor que el de los lenguajes interpretados.
 A causa de esa mejora en el rendimiento, muchos lenguajes
interpretados, de hecho, se compilan para convertirlos en
bytecode y después son ejecutados por un intérprete de
bytecode.
 Entre esos lenguajes se encuentran Perl, PHP y Python. El
código Java se suele trasmitir como bytecode a la máquina
receptora, que utiliza un compilador just-in-time para traducir el
bytecode en código máquina antes de su ejecución.
La Máquina Virtual de Java
(JVM)
 La siguiente liga nos lleva a la información de
lo que es la JVM y mas sobre Java
bytecodes.
III OPTIMIZACION DE
CODIGO
Tipos de Optimización
 Idealmente, los compiladores deben producir código objeto tan
bueno como el producido a mano por un programador experto.
 En la realidad, este objetivo raramente es alcanzado.
 Sin embargo, el código puede ser mejorado en tiempo y espacio
usando algoritmos eficientes.
 Mejorar el código por un compilador es llamado optimización
del código, aunque el término optimizar no es el adecuado (mas
bien mejorar).
 Los tipos de optimización de un compilador pueden ser: Locales,
Globales y de Bucles.
 También los tipos de optimización se pueden dividir en
independientes y dependientes de la máquina.
Introducción
•Objetivo: Mejorar cód. objeto final,
preservando significado del programa
•Factores a optimizar :
velocidad de ejecución
tamaño del programa
necesidades de memoria
•Se sigue una aproximaci´on conservadora
•! No se aplican todas las posibles
optimizaciones, solo las “seguras”
Clasificación de las optimizaciones

En función de la dependencia de la arquitectura


Dependientes de la máquina:
• Aprovechan características específicas de la máquina
objetivo
• asignación de registros, uso de modos de direccionamiento
• uso instrucciones especiales (IDIOMS)
• relleno de pipelines, predicción de saltos, aprovechamiento
estrategias de mem. caché, etc..

Independientes de la máquina:
• Aplicables en cualquier tipo de máquina objetivo
• ejecución en tiempo de compilaci´on
• eliminación de redundancias
• cambios de órden de ejecución, etc..
IV Generación de Código
 La función de un generador de código es
básicamente la traducción de la salida del análisis
sintáctico y semantico (el código intermedio) a una
secuencia equivalente de instrucciones que pueden
ejecutarse en la maquina destino.
 El código debe ser correcto y optimo.
 El generador de código toma las desiciones
básicas:
 Asignar Registros.- Registros generales, Prop. Específicos,
pares, impares, registros índices, etc.
 Selección de Instrucciones.- La mejor secuencia de
instrucciones (la optima). Por ejemplo elegir INC en lugar
de MOV – ADD.
 En el caso de nuestro compilador, una vez que se ha creado un
árbol de ensamblador, el próximo paso es crear código
ensamblador para una máquina específica.

 Generaremos código ensamblador por medio de usar “tiles” para


rellenar huecos, con el árbol de ensamblador abstracto.

 Vamos a crear un conjunto de “tiles” o rellenos cada uno de los


cuales pueden cubrir una porción pequeña de un árbol de
ensamblador abstracto.

 Para generar el ensamblador real para un árbol de ensamblador


específico, primero cubriremos el árbol con “tiles”, asegurándose
que todas las partes del árbol estén cubiertas y que no existan
“tiles” traslapados. Después, se producirá el ensamblador
asociado con cada “tile”.
Código Ensamblador Objeto
Usaremos un subconjunto del ensamblador para MIPS
Instruction Description
lw rt, <offset> (base) Add the constant value <offset> to the register base to get an address. Load the contents of this address into the
register rt. Rt = M[base + <offset>]

sw rt, <offset> (base) Add the constant value <offset> to the register base to get an address. Store the contents of rt into this address.
M[base + <offset>] = rt

add rd, rs, rt Add contents of register rs and rt, put result in register rd.
sub rd, rs, rt Subtract contents of register rt from rs, put result in register rd.
addi rt, rs, <val> Add the constant value <val> to register rs, put result in register rt.

mult rs, rt Multiply contents of register rs by register rt, put the low order bits in register LO and the high bits in register HI

div rs, rt Dive contents of register rs by register rt, put the quotient in register LO and the remainder in register HI

mflo rd Move contents of the special register LOW into the register rd.
j <target> Jump to the assembly label <target>.
jal <target> jump and link. Put the address of the next instruction in the return register, and then jump to the address <target>.
Used for function and procedure calls.
jr rs Jump to the address stored in register rs. Used in conjunction with jal to return from function and procedure calls

slt rd, rs, rt if rs < rt, rd = 1, else rd = 0

beq rs, rt, <target> if rs == rt, jump to the label <target>


bne rs, rt, <target> if rs ≠ rt, jump to the label <target>
bltz rs, <target> if rs < 0, jump to the label <target>
Bgez rs, <target> if rs ≥ 0, jump to the label <target>
Registros que usaremos para generar Código

Mnemonic SPIM Description


Name Name

$FP $fp Frame pointer – points to the top of the current activation record

$SP $sp Stack Pointer – Used for the activation record stack

$ESP $esp Expression Stack Pointer – The next expression stack holds temporary values for
expression avaluations

$result $v0 Result Register – Holds the return value for functions

$return $ra Result Register – Holds the return address for the current function

$zero $zero Zero register – This register always has the value 0

$ACC $t0 Accumulator Register – Used for expressioncalculation

$t1 $t1 General Purpose Register

$t2 $t2 General Purpose Register

$t3 $t3 General Purpose Register


Tiling Simple
 Usaremos una pila para guardar valores temporales en
expresiones. Esta pila estará además de la pila normal de RA.
 Primero describiremos una forma de producir código ineficiente.
 Después veremos como producir código mas eficiente.
 Tiling simple está basado en un recorrido post-order del árbol de
ensamblador abstracto.
 Esto es, después de cubrir el árbol con tiles, emitiremos código para
cada tile en una forma post-order.
 Primero emitiremos recursivamente tiles para cada uno de los sub-
árboles del árbol de ensamblador, de izquierda a derecha.
 Después, emitiremos el código para el tile de la raíz del árbol.
Ejemplos:
Tiling Simple para Árboles de Expresiones.
El código asociado con el tile colocará el valor de la expresión en el tope de la
pila de expresiones.

Expresiones de Constantes
Considere el siguiente árbol:
Constant(5)
El código asociado con este tile necesita poner un 5 en el tope de la pila de
expresiones.

addi $t1, $zero, 5 % Load the constant value 5 into the register $t1
sw $t1, 0($ESP) % Store $t1 on the top of the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer

En general, el tile para cualquier valor constante x es:

addi $t1, $zero, x % Load the constant value into x the register $t1
sw $t1, 0($ESP) % Store $t1 on the top of the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer
Operaciones de Aritmética Binaria

Constant(3) Constant(4)

En lugar de tratar de cubrir todo el árbol con un tile, usaremos tres. La


constante 4 y 5 pueden cubrirse cada una con un tile de constante, y
usaremos un tile “+” para cubrir la suma.

Constant(3) Constant(4)
¿Cómo debe ser el código para +?
Recordemos que emitimos código en post-order (subárbol
izquierdo, subárbol derecho y luego raíz). Podemos asumir
que los valores de los operandos de la izquierda y la
derecha de el + ya están en la pila cuando el código de el tile
+ es emitido. Ntonces, todo lo que necesitamos es hacer es
sacar (pop off) esos valores , hacer la suma y meter (push)
el resultado de regreso a la pila. El código para el tile + es:

lw $t1, 8(ESP) % Load the first operand into temporary $t1


lw $t2, 4(ESP) % Load the second operand into temporary $t2
add $t1, $t1, $t2 % Do the addition. Storing result in $t1
sw $t1, 8(ESP) % Store the result on the expression stack
add $ESP, $ESP, 4 % Update the expression stack pointer
y el código para toda la expresión es:

addi $t1, $zero, 3 % Load the constant value 3 into the register $t1
sw $t1, 0($ESP) % Store $t1 on the top of the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer
addi $t1, $zero, 4 % Load the constant value into 4 the register $t1
sw $t1, 0($ESP) % Store $t1 on the top of the expression stack
addi $ESP, $ESP, -4 %Update the expression stack pointer
lw $t1, 8($ESP) % Load the first operand into temporary $t1
lw $t2, 4($ESP) % Load the second operand into temporary $t2
add $t1, $t1, $t2 % Do the addtion, storing result in $t1
sw $t1, 8(ESP) % Update the expression stack pointer
Registros
Considere el árbol de expresión
Register(FP)
Esto ocupa un tile.

sw $FP, 0($ESP)
addi $ESP, ESP, -4

Combinando otros tiles discutidos antes, el árbol

Register(FP) Constant(8)

Puede ser los tiles:


-

Register(FP) Constant(8)
Produciendo el árbol

sw $FP, 0($ESP) % Store frame pointer on the top of the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer
addi $t1, $zero, 8 % Load the constant value 8 into the register $t1
sw $t1, 0($esp) % Store $t1 on the top of the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer
lw $t1, 8($ESP) % Load the first operand into temporary $t1
lw $t2, 4($ESP) % Load the second operand into temporary $t2
sub $t1, $t1, $t2 % Do the subtraction, storing result in $t1
sw $t1, 8($ESP) % Store the result on the expression stack
add $ESP, $ESP, 4 % Update the expression stack pointer
Operaciones relacionales
ARBOL ABSTRACTO:

>

Constant(3) Constant(4)

TILES:

>

Constant(3) Constant(4)
El código para > es:

lw $t1, 8($ESP)
lw $t2, 4($ESP)
slt $t1, $t2, $t1
sw $t1, 8($ESP)
addi $ESP, $ESP, 4

Thus, one posibility for the code for the == tile is:

lw $t1, 8($ESP)
lw $t2, 4($ESP)
slt $t3, $t2, $t1 % $t3 = (x < y)
slt $t2, $t1, $t2 % $t2 = (y < x)
add $t1, $t1, $t2 % $t1 = (x < y) I I (y <x)
sub $t1, $zero, $t1 %
addi $t1, $t1, 1 %
sw $t1, 8($ESP)
addi $ESP, $ESP, 4
Acceso a Memoria
Un nodo en memoria es solo un “dereferencia” a memoria (indexamiento). Entonces, el
código de un nodo en memoria es:

Lw $t1, 4 ($ESP) % Pop the address to dereference off the top of the
% expression stack
lw $t1, 0($t1) % Dereference the pointer
sw $t1, 4($ESP) % Push the result back on the expression stack

Ejemplo: El árbol de expresión Memory


Para una variable simple es.
-

Register(FP) Constant(12)
Los tiles son: Memory

Register(FP) Constant(12)
Que resulta en el siguiente ensamblador:
sw $FP, 0($ESP) % Store frame pointer on the top of the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer
addi $t1, $zero, 12 % Load the constant value 12 into the register $t1
sw $t1, 0($ESP) % Store $t1 on the top of the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer
lw $t1, 4($ESP) % Load the first operand into temporary $t1
lw $t2, 8($ESP) % Load the second operand into temporary $t2
sub $t1, $t1, $t2 % Do the subtraction, storing result in $t1
sw $t1, 8($ESP) % Store the result on the expression stack
add $ESP, $ESP, 4 % Update the expression stack pointer
lw $t1, $4($ESP) % Pop the address to dereference off the top of
% the expression stack
lw $t1, $0($t1) % Dereference the pointer
sw $t1, 4($ESP) % Push the result back on the expression stack

El resultado de este código es: El valor que estaba en memoria (FP – 12) es empujado (pushed onto)
en el tope de la pila de expresiones.
Llamadas a Funciones
Para implementar una llamada a función, necesitamos primero copiar
todos los parámetros de la función en el tope de la pila de llamadas
(opuesta a la pila de expresiones), y después saltar al inicio de la función.
Cuando la función regresa, necesitamos sacar los argumentos del tope de
la pila de llamadas, y meter el valor de retorno de la función al tope de la
pila de expresiones.
Ejemplo: Considere el siguiente árbol de expresión, el cual resulta de la
llamada a función foo(9,3).
Call(“Foo”)

Constant(9) Constant(3)
Call(“Foo”)

Constant(9) Constant(3)
El lenguaje ensamblador asociado con el tile Call es:
lw $t1 4($ESP) % Pop the first argument off the expression stack
sw $t1 0($Sp) % Store first argument on the call stack
lw $t1 8($ESP) % Pop second argument off the expression stack
sw $t1 -4($SP) % Store second argument on the call stack
addi $SP, $SP, -8 % Update call stack pointer
addi $ESP, $ESP, 8 % Update expression stack pointer
jal foo % Make the finction call
addi $SP, $SP, 8 % Update call stack pointer
sw $result, 0($ESP) % Store result of function on expression stack
Addi $ESP, $ESP, -4 % Update expression stack pointer
Así, el ensamblador para todo el árbol de expresión es:

addi $t1, $zero, 9 % Load the constant value 9 into the register
$t1
sw $t1, 0($ESP) % Store $t1 on the top of the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer
addi $t1, $zero, 3 % Load the constant value into 3 the register
$t1
sw $t1, 0($esp) % Store $t1 on the top of the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer
lw $t1 4($ESP) % Pop the first argument off the expression
stack
sw $t1 0($SP) % Store first argument on the call stack
lw $t1 8($ESP) % Pop second argument off the expression
stack
sw $t1 -4($ESP) % Store second argument on the call stack
addi $SP, $SP, -8 % Update call stack pointer
addi $ESP, $ESP, 8 % Update expression stac pointer
jal foo % Make the function call
addi $SP, $SP, 8 % Update call stack pointer
sw $result, 0($ESP) % Store result of function on expression stack
addi $ESP, $ESP, -4 % Update expression stack pointer
Tiles de Árboles de Estatutos

Mientras que el código de ensamlador para árboles de expresiones


mete (push) el valor de la expresión en el tope de la pila de
expresiones, el código ensamblador para árboles de estatutos
implementa el estatuto descrito por el árbol.

Árboles de Etiquetas
El subárbol Label(“label1”) puede ser cubierto con solo un tile. El
código para ese tile es:
Label1:

Árboles para Move


El lado izquierdo de un MOVE necesita que sea un registro o una
localidad de memoria.
Ejemplo:

Move

Register(r1) Constant(8)
Será dividido en los tiles:

Move

Register(r1) Constant(8)

Considere el tile del MOVE registro:

Move

Register(r1)

Cuando el código para este tile es ejecutado, el valor que es cargado


al registro está ya en el tope de la pila de expresiones. Entonces, todo
lo que necesitamos hacer es sacar el valor de la pila de expresiones y
meterlo al registro.
El tile del MOVE tiene el siguiente código asociado:

lw $r1, 4($ESP) % load the rhs of the move into a register


addi $ESP, $ESP, 4 % update expression stack pointer

Todo el código para el árbol es:

addi $t1, $zero, 8 % Store the constant 8 in a register


sw $t1, 0($ESP) % Push the value on the expression stack
addi $ESP, $ESP, -4 % Update the expression stack pointer
lw $r1, 4($ESP) % load the rhs of the move into a register
addi $ESP, $ESP, 4 % update expression stack pointer

Veamos ahora un MOVE a memoria. Consideremos el siguiente árbol:


Move

Memory Constant(4)

Register(FP)

El árbol puede tener los tiles:


Move

Mem Constatnt(4)

Register(FP)

El tile MOVE a memoria es:


Move
Necesita sacar dos valores del tope de la pila – el destino del MOVE y
el valor a almacenarse.
lw $t1, 8($ESP)
lw $t2, 4($ESP)
sw $t2, 0($t1)
addi $ESP, $ESP, 8

Todo el árbol se puede implementar con el sig. Código:


sw $FP 0($ESP)
addi $ESP, $ESP, -4
addi $t1, $ZERO, 4
sw $t1, 0($ESP)
addi $ESP, $ESP, -4
lw $t1, 8($ESP)
lw $t2, 4($ESP)
sw $t2, 0($t1)
Addi $ESP, $ESP, 8
Árboles para Estatutos Jump
Estatutos árbol como Jump(“label”) se pueden realizar con un solo tile:
j label

Árboles para Jump condicional


Considere el siguiente árbol de estatuto:

CondJump(“jumplab”)

<

Constant(3) Constant(4)

Podemos crear los siguientes tiles:


Tile 4
CondJump(“jumplab”)

Tile 3
<
Tile 1 Tile 2
Constant(3) Constant(4)
El código que emitimos para el subárbol de la expresión jump
condicional pone el valor de la expresión en el tope de la pila de
expresiones. Asi, necesitamos sacar la expresión del tope de la pila de
expresiones, y saltar a la etiqueta “jumplab” si la expresión es
verdadera. Todo el estatuto se implementa con el código:

addi $t1, $zero, 3


sw, $t1, 0($ESP)
addi $ESP, $ESP, -4
addi $t1, $zero, 4
sw $t1, 0($ESP)
addi $ESP, $ESP -4
lw $t1, 8($ESP)
lw $t2, 4($ESP)
slt $t1, $t1, $t2
sw $t1, 8($ESP)
addi $ESP, $ESP, 4
bgtz $t1, jumplab % salta a jumplab si $t1 es mayor a cero
Árboles de Secuencias
¿Qué hacemos después que creamos el código del árbol de la parte
izquierda y el árbol de la parte derecha? ¡nada! No existe código
asociado pues los dos árboles son ya traducidos.

Estatutos Call
Son muy similares a las llamadas a funciones. La única diferencia es
que en el call no existe resultado en el return. Esto significa que
después que termina el procedimiento, no necesitamos copiar el
resultado a la pila de expresiones.

Você também pode gostar