Escolar Documentos
Profissional Documentos
Cultura Documentos
Autómatas II
Dr. Ramón Zatarain Cabada
Repaso
¿Qué es y que estudia la P. de S. ?
Ensambladores …. (notas)
Compiladores……. (notas)
Intérpretes……...... (notas)
Generador de Código para Compiladores
(Compilador de Compiladores)
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.
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
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
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
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
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
B S1 S2
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:
EE+E EE-E EE*E
EE/E Eid Enum
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
EJEMPLO: Z:= X + Y – X * Y
ADD X Y VAR1
MUL X Y VAR2
SUB VAR1 VAR2 VAR3
STORE VAR3 Z
(cont.)
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.
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
$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
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
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)
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:
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
Register(FP) Constant(8)
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
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
Árboles de Etiquetas
El subárbol Label(“label1”) puede ser cubierto con solo un tile. El
código para ese tile es:
Label1:
Move
Register(r1) Constant(8)
Será dividido en los tiles:
Move
Register(r1) Constant(8)
Move
Register(r1)
Memory Constant(4)
Register(FP)
Mem Constatnt(4)
Register(FP)
CondJump(“jumplab”)
<
Constant(3) Constant(4)
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:
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.