CURSO: PROGRAMACION DE SISTEMAS Dr. Ramón Zatarain Cabada
ELEMENTOS DEL CURSO <ul><li>Programa del Curso.....  PDF </li></ul><ul><li>Datos del Profesor ….  RZC </li></ul><ul><li>Co...
Unidad I  Introducción a la Programación de Sistemas
1.1 ¿Qué es y que estudia la P. de S. ? <ul><li>Son los programas que residen en un sistema de computación. Su función es ...
1.2 Herramientas desarrolladas con la P de S <ul><li>Ejemplos: </li></ul><ul><ul><ul><li>       Compiladores (javac) </li>...
1.3 Lenguajes <ul><li>Naturales (Traductores de Ingles-Español, Ingles-Ruso, etc) </li></ul><ul><li>Artificiales (Compilad...
1.4 Traductor y su Estructura <ul><ul><li>Ensambladores …. ( notas )  </li></ul></ul><ul><ul><li>Compiladores……. ( notas )...
1.5 Generador de Código para Compiladores (Compilador de Compiladores) <ul><li>Definición : Un compilador de compiladores ...
Unidad II Introducción al Diseño de Lenguajes de Programación
<ul><li>Visión del Problema. </li></ul><ul><li>Consideraciones Preliminares. </li></ul><ul><li>Objetivos y filosofías del ...
Unidad III  Análisis de Léxico
3.1 Introducción a los Autómatas Finitos y Expresiones Regulares <ul><li>(ver apuntes de Lenguajes y Autómatas). </li></ul>
3.2 Analizador de Léxico . <ul><li>Descompone la entrada en palabras individuales llamadas “tokens”. El analizador de léxi...
Analizador de Léxico (cont.) <ul><li>Tokens de puntuación como IF, VOID y RETURN son llamadas palabras reservadas y en la ...
3.3 Manejo de “Buffers” <ul><li>El analizador de léxico (scanner) y el analizador de sintáxis (parser) forman un duo “prod...
3.4 Creación de la Tabla de Símbolos. <ul><li>Checar  notas </li></ul><ul><li>La tabla de símbolos es una estructura de da...
Ejemplo <ul><li>Ejemplo. -  </li></ul><ul><li>Programa X1;  </li></ul><ul><li>          Var X, Y: Integer;  </li></ul><ul>...
3.5 Manejo de Errores de Léxico <ul><li>Errores posibles detectados en el análisis de léxico son: </li></ul><ul><ul><li>Pa...
3.6 Generadores de Código Léxico <ul><li>La construcción de un scanner puede automatizarse por medio de un generador de an...
Ejemplo: Javacc <ul><li>Javacc (Java Compiler Compiler) es un generador de scanners y parsers (https://javacc.dev.java.net...
(cont.) Miparser.jj javacc Miparser.jj Miparser.java ParseException.java TokenMgrError.java otros archivos java javac Mipa...
(cont.) PARSER_BEGIN(PS1) class PS1 {} PARSER_END(PS1) /* Para la expresión regular de la derecha lo de la izquierda será ...
Unidad IV Análisis de Sintáxis
4.1 Introducción a las gramáticas Libres de Contexto y Árboles de derivación  <ul><li>El tipo de gramáticas usadas en los ...
(cont.) <ul><li>Gramática Ambigua. Una gramática es ambigua si puede derivar una oración (cadena) con dos diferentes árbol...
(cont.) S Id := E E + E id E + E id id S Id := E E + E E + E id Id id
(cont.) E E - E E - E 3 1 2 E E - E 1 E - E 2 3
4.2 Diagramas de Sintaxis <ul><li>Un método alternativo al BNF para desplegar las producciones de ciertas gramáticas es el...
4.3 Precedencia de Operadores <ul><li>En los dos ejemplos vistos en sección 4.1, los dos árboles de parsing para 1-2-3 sig...
(cont.) <ul><li>S  E$ nota:  “$” es EOF- marker </li></ul><ul><li>E  E+T E  E-T E  T </li></ul><ul><li>T-->T*F T  T/F...
4.4 Analizador Sintáctico <ul><li>En esta fase se analiza la estructura de la frase del programa.  </li></ul><ul><li>El pa...
(cont.) <ul><li>Existen diferentes técnicas o métodos para realizar un análisis  </li></ul><ul><li>sintáctico “Parsing”. E...
4.4.1 Analizador descendente (LL). <ul><li>Parsing Predictivo   </li></ul><ul><li>Algunas gramáticas son sencillas de anal...
(cont.) <ul><li>Esta técnica se utilizó o popularizó en los años 70 a partir del primer compilador de pascal implementado ...
Programa del Parser Final int if = 1, then = 2, else = 3, begin = 4, end = 5, print = 6, semi = 7,  num = 8, EQ = 9  int t...
(cont.) <ul><li>Un parser predictivo que examina la entrada de izquierda a derecha (left-to-right) en un paso y realiza su...
Eliminación de Recursión por la izquierda <ul><li>Suponer que queremos construír un Parser predictivo para la gramática de...
(cont.) <ul><li>En general, si tenemos producciones X    X   </li></ul><ul><li>y X      donde   no comience con ...
(cont.) <ul><li>Aplicando la transformación a la gramática anterior, obtenemos la nueva equivalente gramática (sin recursi...
Factorización por la izquierda <ul><li>Otro problema en una gramática ocurre cuando dos producciones para el mismo no term...
4.4.2 Analizador ascendente(LR y LALR). <ul><li>La debilidad de las técnicas descendentes LL(k) es que deben predecir que ...
(cont.) <ul><li>Un Parser LR(K) está compuesto de: </li></ul><ul><ul><li>La cadena de entrada </li></ul></ul><ul><ul><li>U...
Algoritmo de Parsing LR (aho,Sethi y Ullman) Tomar el primer token de w$  /* w es la cadena */ Repeat forever begin Sea s ...
Ejemplo: Parser LR(K) S4   s7   g2 s3    a S4   s7   g5   s6 r1  r1   r1 S20  s10 s8   g11 s9 S4   s7   g12   TABLA DE PAR...
Ejemplo (cont.): PILA ENTRADA   ACCION <ul><li>a:=7;B:=c+(d:=5+6,d)$   shift </li></ul>1  id4  := 7;B:=c+(d:=5+6,d)$  shif...
Parsing LALR <ul><li>La técnica de parsing LALR (LookAhead-LR) evita el uso de tablas muy grandes, como las manejadas en l...
  4.5 Administración de tablas de símbolos. <ul><li>Como se estudió en la unidad anterior, durante el análisis de léxico s...
4.6 Manejo de errores sintácticos y su recuperación. <ul><li>Dentro del código del  parser   predictivo   estudiado en cla...
(cont.) <ul><li>Un error sintáctico ocurre cuando la cadena de tokens de entrada no es una oración en el lenguaje. La recu...
Ejemplo: void S ( ) { switch ( tok ) {  case If: eat ( if ); E ( ); eat ( then ); S ( );  eat ( else ); S ( ); break;  cas...
4.7 Generadores de código para analizadores sintácticos <ul><li>Como se vio al final del capítulo 3, Javacc es un generado...
Ejemplo: gramática para estatutos “begin”, “if” y “print” PARSER_BEGIN(MiniParser) public class MiniParser { public static...
(cont. Ejemplo) TOKEN : { <INT: &quot;INT&quot;> | <IF: &quot;if&quot;> | <THEN: &quot;then&quot;> | <ELSE: &quot;else&quo...
Unidad V Análisis Semántico
5.1 Analizador semántico <ul><li>Un compilador no solo tiene que revisar la sintaxis de código fuente, si no también la se...
5.2 Verificación de tipos en expresiones. <ul><li>Cuando mezclamos diferentes tipos en una misma expresión o que llamamos ...
5.3 Conversión de tipos. <ul><li>Algunas veces los tipos de una expresión o estatuto son diferente. </li></ul><ul><li>Por ...
<ul><li>En un parser recursivo-descendente, el código de las acciones semánticas es mezclado dentro del flujo de control d...
(cont.) <ul><li>Los otros tokens no necesitarían tener un valor. </li></ul><ul><li>Por otra parte el tipo asociado a un to...
Recursivo-descendente <ul><li>En un parser recursivo-descendente,  las acciones semánticas son los valores retornados por ...
(cont.) S    E$ E    T E’ E’    + T E’ E’    - T E’ E’      T    F T’ T’    * F T’ T’    / F T’ T’      F    i...
Interprete class Token2 { int kind; Object val; Token2(int k, Object v) {   kind=k;   val=v; } } final int EOF=0, ID=1, NU...
Parser Automáticamente generado <ul><li>Una especificación del parser para javaCC consistiría de un conjunto de reglas gra...
Árboles de Parsing Abstractos <ul><li>Para mejorar la modularidad del compilador, es recomendable separar detalles de la s...
(cont.) <ul><li>Otro ejemplo es en el árbol de parsing: </li></ul>Cuyo árbol sintáctico abstracto sería: + * 8 2 4   L E +...
Ejemplo: <ul><li>La gramática siguiente nos muestra una sintaxis abstracta de un lenguaje para expresiones: </li></ul><ul>...
Árboles de Sintaxis en Java <ul><li>En Java las estructuras de datos para el árbol de sintaxis contienen una clase abstrac...
Programa de clases para Exp public abstract class ExpCh4 { public abstract int eval(); } class PlusExp extends ExpCh4 { pr...
(cont.) <ul><li>Ahora veamos un intérprete para el lenguaje de expresiones de la gramática de sección 4.1.1. Por convenien...
Gramática con acciones semánticas para árboles sintácticos PARSER_BEGIN(InterSinTree) class InterSinTree {} PARSER_END(Int...
VISITADORES <ul><li>Es una técnica de patrones (opuesta a la orientada a objetos) que se puede usar para implementar el ár...
(cont.) <ul><li>A continuación veremos un ejemplo del intérprete de expresiones anterior pero ahora implementado con visit...
Sintáxis Abstracta para MiniJava <ul><li>En la siguiente figura (siguiente diapositiva)  mostramos las clases de la sintax...
Package syntaxtree; Program (MainClass m, ClassDeclList cl) MainClass (Identifier i1, Identifier i2, Statement s) Abstract...
(cont. figura) Abstract class Exp And (Exp e1, Exp e2) LessTha n(Exp e1, Exp e2) Plus (Exp e1, Exp e2) Minu s(Exp e1, Exp ...
Arbol Sintáctico <ul><li>Cada una de las clases que no son listas tiene un método  accept  para usarse con el patrón  visi...
<ul><li>public interface Visitor { </li></ul><ul><ul><li>public void visit(Program n); </li></ul></ul><ul><ul><li>public v...
(cont. Arbol Sintáctico) <ul><li>Podemos construir un árbol sintáctico usando expresiones  new  anidadas. Por ejemplo el á...
5.5 Pila semántica en un analizador sintáctico ascendente (bottom-up). <ul><li>Como fue visto en el capitulo anterior (4),...
5.6 Administración de la tabla de símbolos <ul><li>El análisis semántico conecta las definiciones de las variables con sus...
(cont.) <ul><li>Un ambiente es un conjunto de atados (bindings) denotados por   . Por ejemplo, podemos decir que el ambie...
Ejemplo: <ul><li>Class C { </li></ul><ul><li>int a, int b; int c; </li></ul><ul><li>public void m() { </li></ul><ul><li>Sy...
Implementación de la Tabla <ul><li>Existen dos opciones: El estilo funcional donde cuando  z1  existe y  z2  es creado,  z...
Múltiple Tablas de Símbolos <ul><li>En algunos LP pueden existir varios ambientes a la vez: Cada módulo, o clase o registr...
(cont.) <ul><li>Al anlizar los 2 programas anteriores, sea z0 el ambiente base conteniendo funciones predefinidas, y sea <...
(cont.) <ul><li>En ML, N es compilado usando el ambiente z0+z2 para buscar los identificadores en la tabla;D es compilado ...
TABLAS DE SIMBOLOS EN LENGUAJES IMPERATIVOS <ul><li>Un programa grande puede contener miles de distintos identificadores. ...
(cont.) <ul><li>Considere  z +{ a  t2 } cuando  z  ya contiene  a  t1 . La función  insert  deja a  t1 en el “bucket” y...
SIMBOLOS <ul><li>Para evitar comparaciones innecesarias de cadenas podemos convertir cada cadena a un  símbolo , y así tod...
(cont.) <ul><li>Los ambientes son implementados en la clase  Symbol.Table  como  Tables  mapeando  Symbols  a ligados ( bi...
(cont.) <ul><li>Cuando la atadura x  b es metido a la tabla (table.put(x,b)),  x  es dispersado a un índice  i,  y un obj...
Chequeo de Tipos en MiniJava <ul><li>¿Con que se llena una tabla de símbolos? Esto es, ¿Qué es la ligadura o “binding”?  <...
(cont.) <ul><li>Por ejemplo, considere la siguiente figura, que muestra un programa y su tabla de símbolos. </li></ul>Clas...
(cont.) <ul><li>Los tipos primitivos en MiniJava son int y boolean; todos los otros tipos son arreglo de enteros o nombres...
(cont.) <ul><li>La primera fase de el checador de tipos se puede implementarse por medio de un visitador que visite los no...
Método Visitador Class ErrorMsg { boolean anyErrors; void complain (String msg) { anyErrors = true; System.out.println(msg...
(cont.) <ul><li>La segunda fase del checador de tipos puede ser implementada por un visitador que cheque tipos de todas lo...
Método Visitador para expresiones Plus // Exp e1, e2; Public Type visit(Plus n) { if (! (n.e1.accept(this) instanceOf Inte...
5.7 Manejo de errores semánticos. <ul><li>Cuando el checador de tipos detecta un error de tipos o un identificador no decl...
REGISTROS DE ACTIVACION <ul><li>En casi cualquier LP, una función (método) puede tener variables locales que son creadas c...
(cont.) <ul><li>En el siguiente método de Java </li></ul>Int f(int x) { int y= x+x; if (y<10) return f(y); else  return y-...
(cont.) <ul><li>En muchos LP (incluyendo Pascal, C y java), las variables locales son destruidas cuando una función retorn...
MARCOS DE PILA <ul><li>Debido a que se trabaja con bloques de datos por función un “push” y “pop” no funciona. </li></ul><...
(cont.) <ul><li>El diseño de la estructura de los marcos es de acuerdo con la arquitectura y el LP que se compila.  </li><...
Ejemplo: Un marco de pila Argumentos de entrada Apuntador Del marco Argumentos De salida Apuntador De pila Argumento  n … ...
Marco de Pila <ul><li>Los argumentos de entrada son los pasados por el llamador (técnicamente son parte del marco anterior...
El Apuntador de Marco (FP) <ul><li>Suponer que una función g(…) llama la función f(a 1 ,…a n ). Diremos que  g  es el llam...
Registros <ul><li>Por eficiencia, es importante mantener las variables locales, resultados intermedios y otros valores en ...
Pase de Parámetros <ul><li>Estudios actuales han mostrado que raramente una función pasa mas de 4 parámetros. </li></ul><u...
Direcciones de Retorno <ul><li>Si  g  llama a  f , entonces si la instrucción  call  dentro de  g  está en dirección  a , ...
Registros vs. Memoria <ul><li>Registros siempre deben usarse en asignación a menos que: </li></ul><ul><ul><li>La variable ...
Ligas Estáticas (Static Links) <ul><li>En LP que admiten funciones anidadas (Pascal,ML y Java) las funciones de mas adentr...
Programa de funciones Anidadas Type tree= {key: string, left: tree, right: tree} Function prettyprint(tree:tree): string= ...
Ligas Estáticas (cont.) <ul><li>Existen varios métodos para solucionar lo anterior: </li></ul><ul><ul><li>Siempre que una ...
(cont.) <ul><li>Línea 21:  prettyprint  llama  show , pasando el apuntador del marco del propio  prettyprint  como una lig...
Unidad VI Generación de Código Intermedio
6.1 Lenguajes intermedios. <ul><li>El código intermedio en una estructura de código cuya complejidad está entre un código ...
(cont.) <ul><li>Ventajas de producir código intermedio:  </li></ul><ul><li>Más fácil de producir código objeto después, si...
6.2 Notaciones. <ul><li>Infija.  Es la notación habitual. El orden es primer operando, operador, segundo operando.  </li><...
(cont.) <ul><li>Postfija. El orden es primer operando, segundo operando, operador.  </li></ul><ul><li>Ejemplo : La expresi...
6.3 Representación de código intermedio. <ul><li>Existen muchas clases de representaciones </li></ul><ul><li>intermedias. ...
Notación Polaca <ul><li>También conocida como notación postfija. Se utiliza como se dijo anteriormente en máquinas de Pila...
Código P <ul><li>Se usó como código intermedio y objeto en las primeras implementaciones de Pascal. </li></ul><ul><li>El c...
Ejemplo: <ul><li>Insn. Stack  Stack  Description  </li></ul><ul><li>before after  </li></ul><ul><li>adi  i1 i2  i1+i2  add...
Triples y Cuadruplos (Código de 3 Direcciones) <ul><li>Ha sido uno de los mas populares. Sus instrucciones constan de 3 di...
(cont.) <ul><li>EJEMPLO:               Z:= X + Y – X * Y  </li></ul><ul><li>ADD                   X          Y            ...
(cont.) <ul><li>EJEMPLO : (Estructure de Control): </li></ul><ul><li>If (a==b) </li></ul><ul><li>a=0; </li></ul><ul><li>el...
(cont.) <ul><li>En el código de 2 direcciones se evita el uso se variables temporales. El formato es:  </li></ul><ul><li>O...
Java Bytecodes <ul><li>El bytecode es un código intermedio más abstracto que el código máquina. Habitualmente viene a ser ...
(cont.) <ul><li>Como código intermedio, se trata de una forma de salida utilizada por los implementadores de lenguajes par...
(cont.) <ul><li>Su ventaja es su portabilidad: el mismo código binario puede ser ejecutado en diferentes plataformas y arq...
La Máquina Virtual de Java  (JVM) <ul><li>La siguiente liga nos lleva a la información de lo que es la  JVM  y mas sobre J...
Árboles de Ensamblador <ul><li>Un paso antes de generar Ensamblador Real </li></ul><ul><li>Permite mas fácil optimización ...
Implementación de variables <ul><li>En Java las variables locales son almacenadas en la pila y en registros. </li></ul><ul...
Por ejemplo, la función: Int foo() { int a; int b; /* body of foo */ } FP SP Tendría el registro de activación Existen dos...
(cont.) <ul><li>Como el FP siempre apunta al comienzo del RA actual, podemos acceder a las variables locales usando un  of...
Formato completo de RA FP SP Previous Activation Record Current Activation Record Saved Registers Local Variables Input Pa...
Considere el siguiente programa: void foo(int a, int b); void foo(int c, int d); void main() { int u; int v;  /* Label A *...
En  label A  de el programa, la pila de RA se miraría de la forma siguiente FP SP Activation Record for main La variable l...
En etiqueta B los RA se mirarían así: FP SP Activation Record for main Activation Record for bar Activation Record for foo...
(cont.) <ul><li>La variable  y  en función  foo  puede accesarse al examinar la localidad de memoria apuntada por  FP . La...
Ensamblador Abstracto <ul><li>El ensamblador abstracto es dividido en árboles de estatutos y árboles de expresiones. Árbol...
(cont.) <ul><li>CallExpression. Contiene una etiqueta de lenguaje ensamblador y un árbol de expresión por cada uno de los ...
<ul><li>Memoria. Solo se denota la dirección de memoria.  </li></ul><ul><li>Ejemplo: </li></ul>Casi nunca se manejan direc...
<ul><li>Existen 8 tipos de árboles de estatutos: </li></ul><ul><ul><li>Move. Tienen dos subárboles – el´subárbol izquierdo...
Ejemplos: (asumiremos que wordsize=4 y que un entero se guarda en una palabra) void foo(int a, int b) { int x; int y; bool...
X=1; Move Memory Constant(1) Register(FP)
y = a * b; Move Memory Operator (-) Register(FP) Constant(4) Operator(*) Memory Memory Operator (+) Operator (+) Register(...
Y++; Move Memory Operator (-) Constant(4) Operator(+) Memory Constant(1) Operator (-) Register(FP) Constant(4) Register(FP)
bar(y,x+1,a); CallStatement(“bar”) Memory Operator (-) Constant(4) Register(FP) Operator (+) Memory Constant(1) Register(F...
x=function(y+1,3); Move Memory Register(FP) Callexpression(“function”) Constant(3) Constant(1) Operator(+) Memory Operator...
if (x > 2) z = true; else z = false; Sequential ConditionalJump(“iftrue”) Operator(>) Memory Constant(2) Sequential Sequen...
Creación de Ensamblador Abstracto <ul><li>Variables </li></ul><ul><ul><li>Variables base </li></ul></ul><ul><ul><li>La var...
<ul><ul><li>Variables Arreglo: Son almacenadas en el heap, y usando un apuntador a la base del arreglo el cual se almacena...
La variable local x es almacenada en la pila, igual que la dirección base del arreglo A y del arreglo B. Stack Heap x A B ...
¿Cómo debemos representar el arreglo A[3] Register(FP) Memory Operator(-) Memory Operator(*) Operator(-) Constant(WORDSIZE...
¿ Y A[x] ? Memory Operator(-) Memory Operator(*) Operator(-) Register(FP) Constant(WORDSIZE) Constant(WORDSIZE) Memory Reg...
¿ Y para A[B[2]] ? Memory Operator(-) Memory Operator(*) Operator(-) Register(FP) Constant(WORDSIZE) Constant(WORDSIZE) Me...
Arreglos Multidimensionales se manejan de manera similar. Ejemplo: void twoDarray { int i; int c [ ] [ ] ; C = new int [3]...
El RA y el Heap se ven así: Stack Heap C[0] C[1] C[2] C[0] [0] C[0] [1] C[1] [0] C[1] [1] C[2] [0] C[2] [1] i C FP SP Save...
La variable C sería representada así: Memory Operator(-) Register(FP) Constant(WORDSIZE)
La variable C[2] así: Memory Operator(-) Memory Operator(*) Operator(-) Register(FP) Constant(WORDSIZE) Constant(WORDSIZE)...
La variable c[2] [1]  Memory Operator(-) Memory Operator(*) Operator(-) Memory Operator(*) Constant(WORDSIZE) Constant(1) ...
Variables Instanciadas Son muy similares a las variables arreglo. La única diferencia es que en las variables arreglo, el ...
¿Cuál es el árbol de ensamblador para s.y ? Memory Register(FP) Él árbol de  s : Para obtener  y  de  s : Memory Memory Op...
El árbol de la variable s.x: Memory Memory Register(FP) El árbol para  .A[3]: Memory Operator(-) Memory Operator(*) Operat...
Estatutos <ul><li>Estatutos de Asignación </li></ul><ul><li><variable> = <expresión> </li></ul><ul><li>es representada por...
Ejemplo: Void main() { int x; int y;  y = x + 4; }  Move Memory Operator(+) Operator(-) Constant(4) Memory Register(FP) Re...
<ul><li>Estatuto If </li></ul><ul><li>if (<test>) <estatuto1> else <estatuto2> </li></ul><ul><li>se puede traducir en </li...
Y se representa con el árbol: Sequential ConditionalJump(“iftrue”) <test> Sequential  Sequential Sequential Sequential Lab...
<ul><li>Estatuto  while: </li></ul><ul><li>while (<test>) <estatuto> </li></ul><ul><li>se puede traducir en </li></ul><ul>...
<ul><li>Estatuto  for: </li></ul>for (<initialize>;<test>;<increment>) <estatement> es equivalent a: <initialize> while (<...
<ul><li>Estatuto Return: </li></ul><ul><li>Cuando un estatuto  return  es ejecutado, el valor que es retornado necesita sa...
<ul><li>Definiciones de Función o Métodos: </li></ul><ul><li>El ensamblador abstracto debe: </li></ul><ul><ul><li>Guardar ...
Creación de Árboles de Ensamblador Abstracto (AAT) en Java <ul><li>Para construir AAT en Java, primero definimos una inter...
Etiquetas Se necesitan tanto para las llamadas a métodos como para los “if” y los ciclos “while” y “for”. La siguiente cla...
Interfase para Construir Árboles de Ensamblador Abstracto import java.util.Vector; public interface AATBuildTree { public ...
EJEMPLOS: void foo(int a) { int b; int c; if (a > 2) b = 2; else c = a+ 1; }  Sea  bt  una instancia de una clase que impl...
Igual, el ensamblador abstracto para el estatuto  b = 2;  y  c = a + 1 , puede crearse con: AATEstatement  s1, s2; s1 = bt...
<ul><li>public AATSatement functionDefinition(AATStatemen body, int framesize, Label start, Label end);  El árbol de ensam...
Modificaciones al Analizador Semántico <ul><li>En lugar de crear un módulo separado que recorra el AST de nuevo para const...
Ejemplo: Para analizar un AST: Integer_Literal(3) El analizador semántico actualmente retorna  IntegerType .  Para analiza...
Cuando se analiza el AST: + Integer_Literal(3)  Integer_Literal(4) Primero se llama el método  accept  de los subárboles, ...
VIII Generación de Código <ul><li>La función de un generador de código es básicamente la traducción de la salida del análi...
<ul><li>En el caso de nuestro compilador, una vez que se ha creado un árbol de ensamblador, el próximo paso es crear códig...
Código Ensamblador Objeto Usaremos un subconjunto del ensamblador para MIPS if  rs  ≥ 0, jump to the label <target> Bgez r...
Register  that we will use for code generation General Purpose Register $t3 $t3 General Purpose Register $t2 $t2 General P...
<ul><li>Usaremos una pila para guardar valores temporales en expresiones. Esta pila estará además de la pila normal de RA....
Ejemplos: Tiling Simple para Árboles de Expresiones . El código asociado con el tile colocará el valor de la expresión en ...
Operaciones de Aritmética Binaria + Constant(3) Constant(4) En lugar de tratar de cubrir todo el árbol con un tile, usarem...
¿Cómo debe ser el código para +?  Recordemos que emitimos código en post-order (subárbol izquierdo, subárbol derecho y lue...
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($E...
Registros Considere el árbol de expresión Register(FP) Esto ocupa un tile.  sw  $FP,  0($ESP) addi  $ESP, ESP, -4   Combin...
Produciendo el árbol sw  $FP, 0($ESP) % Store frame pointer on the top of the expression stack  addi $ESP, $ESP, -4 % Upda...
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Upcoming SlideShare
Loading in …5
×

Curso prog sist

863 views
669 views

Published on

curso de compiladores

0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
863
On SlideShare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
26
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

Curso prog sist

  1. 1. CURSO: PROGRAMACION DE SISTEMAS Dr. Ramón Zatarain Cabada
  2. 2. ELEMENTOS DEL CURSO <ul><li>Programa del Curso..... PDF </li></ul><ul><li>Datos del Profesor …. RZC </li></ul><ul><li>Contenido del Curso…. Diaps </li></ul><ul><li>Proyectos …. Archs </li></ul><ul><li>Software y Herramientas </li></ul>
  3. 3. Unidad I Introducción a la Programación de Sistemas
  4. 4. 1.1 ¿Qué es y que estudia la P. de S. ? <ul><li>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. </li></ul><ul><li>La P. de S. estudia como están implementados cada uno de los programas de un Sistema (ver notas ). </li></ul>
  5. 5. 1.2 Herramientas desarrolladas con la P de S <ul><li>Ejemplos: </li></ul><ul><ul><ul><li>      Compiladores (javac) </li></ul></ul></ul><ul><ul><ul><li>      Ensambladores (Masm) </li></ul></ul></ul><ul><ul><ul><li>      Interpretes (Visual Basic) </li></ul></ul></ul><ul><ul><ul><li>      Ligadores (Link) </li></ul></ul></ul><ul><ul><ul><li>      Cargadores </li></ul></ul></ul><ul><ul><ul><li>      Sistema Operativo (Windows) </li></ul></ul></ul><ul><ul><ul><li>      Utilerías de Sistemas (Debugger) </li></ul></ul></ul>
  6. 6. 1.3 Lenguajes <ul><li>Naturales (Traductores de Ingles-Español, Ingles-Ruso, etc) </li></ul><ul><li>Artificiales (Compiladores de LP como Java, C++, Ada, etc.) </li></ul>
  7. 7. 1.4 Traductor y su Estructura <ul><ul><li>Ensambladores …. ( notas ) </li></ul></ul><ul><ul><li>Compiladores……. ( notas ) </li></ul></ul><ul><ul><li>Intérpretes……...... ( notas ) </li></ul></ul>
  8. 8. 1.5 Generador de Código para Compiladores (Compilador de Compiladores) <ul><li>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 ). </li></ul>
  9. 9. Unidad II Introducción al Diseño de Lenguajes de Programación
  10. 10. <ul><li>Visión del Problema. </li></ul><ul><li>Consideraciones Preliminares. </li></ul><ul><li>Objetivos y filosofías del diseño de los </li></ul><ul><li>lenguajes de programación. </li></ul><ul><li>Diseño detallado. </li></ul><ul><li>Caso de estudio. </li></ul>
  11. 11. Unidad III Análisis de Léxico
  12. 12. 3.1 Introducción a los Autómatas Finitos y Expresiones Regulares <ul><li>(ver apuntes de Lenguajes y Autómatas). </li></ul>
  13. 13. 3.2 Analizador de Léxico . <ul><li>Descompone la entrada en palabras individuales llamadas “tokens”. El analizador de léxico (scanner) toma una cadena de caracteres que forman la entrada y produce una cadena de nombres o identificadores, palabras claves o reservadas (PC) signos o marcas de puntuación, operadores aritméticos y lógicos, constantes (números, cadenas, etc.) y otros. También el scanner tiene como función desechar espacios en blanco y comentarios entre los tokens, otra función podría ser crear la tabla de símbolos. </li></ul><ul><li>Ejemplo : </li></ul><ul><li>TIPO EJEMPLOS </li></ul><ul><li>ID foo n14 last </li></ul><ul><li>NUM 73 0 00 515 </li></ul><ul><li>REAL 66.1 .5 10. 1e67 </li></ul><ul><li>IF if </li></ul><ul><li>COMMA , </li></ul><ul><li>NOTEQ != </li></ul><ul><li>LPAREN ( </li></ul>
  14. 14. Analizador de Léxico (cont.) <ul><li>Tokens de puntuación como IF, VOID y RETURN son llamadas palabras reservadas y en la mayoría de los lenguajes no pueden usarse como identificadores. </li></ul>
  15. 15. 3.3 Manejo de “Buffers” <ul><li>El analizador de léxico (scanner) y el analizador de sintáxis (parser) forman un duo “productor-consumidor”. El scanner produce tokens y el parser los consume. </li></ul><ul><li>La implementación de la lectura de los caracteres de la entrada (disco) es usualmente hecha por un buffer (memoria) que contendrá una buena parte de la entrada, desde donde el scanner irá formando los tokens. Esto agiliza la entrada que de otra forma leería carácter por carácter desde el disco. </li></ul>
  16. 16. 3.4 Creación de la Tabla de Símbolos. <ul><li>Checar notas </li></ul><ul><li>La tabla de símbolos es una estructura de datos muy importante en casi todo el proceso de compilación. En ella se guarda durante las primeras fases de compilación los nombres de los identificadores (símbolos) usados en el programa fuente, además de los atributos de cada uno de estos identificadores. Estos identificadores y símbolos junto con sus atributos serán usados posteriormente para realizar funciones como el chequeo de tipos, la asignación de memoria, generación de código objeto etc. </li></ul>
  17. 17. Ejemplo <ul><li>Ejemplo. - </li></ul><ul><li>Programa X1; </li></ul><ul><li>         Var X, Y: Integer; </li></ul><ul><li>                 Z: Real; </li></ul><ul><li>                 Arreglo: Array [1…100] of int </li></ul><ul><li>         Procedure compara (a, b: Integer) </li></ul><ul><li>                   Var n, m: integer; </li></ul><ul><li>                   Begin </li></ul><ul><li>---- </li></ul><ul><li>---- </li></ul><ul><li> End </li></ul><ul><li>Begin </li></ul><ul><li>---- </li></ul><ul><li>---- </li></ul><ul><li>End </li></ul><ul><li>  </li></ul>
  18. 18. 3.5 Manejo de Errores de Léxico <ul><li>Errores posibles detectados en el análisis de léxico son: </li></ul><ul><ul><li>Patrones de tokens que no coincidan con algún patrón válido. Por ejemplo el token #### sería inválido en algunos L.P. </li></ul></ul><ul><ul><li>Caracteres inválidos para el alfabeto del el lenguaje. </li></ul></ul><ul><ul><li>Longitud de ciertos tokens demasiado larga. </li></ul></ul>
  19. 19. 3.6 Generadores de Código Léxico <ul><li>La construcción de un scanner puede automatizarse por medio de un generador de analizadores de léxico. </li></ul><ul><li>El primer programa de este tipo que se hizo popular fue Lex (Lesk, 1975) que generaba un scanner en lenguaje C. </li></ul><ul><li>Hoy en día existen muchos programas de este tipo: Flex, Zlex, YooLex, JavaCC, SableCC, etc. </li></ul>
  20. 20. Ejemplo: Javacc <ul><li>Javacc (Java Compiler Compiler) es un generador de scanners y parsers (https://javacc.dev.java.net/). </li></ul><ul><li>Toma como entrada especificaciones de léxico (expresiones regulares) y sintáxis (gramática de contexto libre) y produce como salida un analizador de léxico y un parser recursivo decendente. </li></ul><ul><li>Se puede usar como pre-procesador de Javacc, a jjtree y a JTB para la construcción del árbol sintáctico. </li></ul>
  21. 21. (cont.) Miparser.jj javacc Miparser.jj Miparser.java ParseException.java TokenMgrError.java otros archivos java javac Miparser.java Miparser.class java Miparser <inputfile mensajes USO DE JAVACC
  22. 22. (cont.) PARSER_BEGIN(PS1) class PS1 {} PARSER_END(PS1) /* Para la expresión regular de la derecha lo de la izquierda será retornado */ TOKEN: { <IF: &quot;if&quot;> |<#DIGIT: [&quot;0&quot;-&quot;9&quot;]> |<ID: [&quot;a&quot;-&quot;z&quot;] ([&quot;a&quot;-&quot;z&quot;]|<DIGIT>)*> |<NUM: (<DIGIT>)+> |<REAL: ((<DIGIT>)+ &quot;.&quot; (<DIGIT>)*) | ((<DIGIT>)* &quot;.&quot; (<DIGIT>)+)> } SKIP: { <&quot;--&quot; ([&quot;a&quot; - &quot;z&quot;])* (&quot;n&quot; | &quot;r&quot; | &quot;rn&quot;)> |&quot; &quot; |&quot;t&quot; |&quot;n&quot; |&quot;r&quot; } void Start(): {} { (<IF> | <ID> | <NUM> | <REAL>)* } EJEMPLO DE ESPECIFICACION PARA GENERAR UN SCANNER
  23. 23. Unidad IV Análisis de Sintáxis
  24. 24. 4.1 Introducción a las gramáticas Libres de Contexto y Árboles de derivación <ul><li>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. </li></ul><ul><li>Un ejemplo de una gramática para un LP simple es la siguiente: </li></ul><ul><li>1) S  S;S 2) S  id := E 3) S  print (L) </li></ul><ul><li>4) E  id 5) E  num 6) E  E + E 7) E  (S,E) </li></ul><ul><li>8) L  E 9) L  L , E </li></ul><ul><li>(ver ejemplo de derivaciones y árboles de parsing) </li></ul><ul><li>A continuación veremos un ejemplo de una gramática para Java en formato BNF ( liga ). </li></ul>
  25. 25. (cont.) <ul><li>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: </li></ul><ul><li>E  id E  num E  E*E E  E/E </li></ul><ul><li>E  E+E E  E-E E  (E) </li></ul><ul><li>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”). </li></ul>
  26. 26. (cont.) S Id := E E + E id E + E id id S Id := E E + E E + E id Id id
  27. 27. (cont.) E E - E E - E 3 1 2 E E - E 1 E - E 2 3
  28. 28. 4.2 Diagramas de Sintaxis <ul><li>Un método alternativo al BNF para desplegar las producciones de ciertas gramáticas es el diagrama de sintaxis . Ésta es una imagen de la producciones que permite al usuario ver las sustituciones en forma dinámica, es decir, verlas como un movimiento a través del diagrama. </li></ul><ul><li>Ejemplo en Java ( liga ) </li></ul>
  29. 29. 4.3 Precedencia de Operadores <ul><li>En los dos ejemplos vistos en sección 4.1, los dos árboles de parsing para 1-2-3 significaba diferentes cosas: (1-2)-3=-4 versus 1-(2-3)=2. Similarmente, (1+2)x3 no es lo mismo que 1+(2x3). Es por eso que gramáticas ambiguas son problemáticas para un compilador. Afortunadamente, una gramática ambigua puede convertirse en una no ambigua. Por ejemplo, si queremos encontrar una gramática no ambigua para el lenguaje de la segunda gramática de sección 4.1, lo podemos hacer por medio de dos operaciones: primero, aplicar orden de precedencia a los operadores y segundo aplicar asociatividad por la izquierda. Con esto, tenemos la sig. gramática: </li></ul>
  30. 30. (cont.) <ul><li>S  E$ nota: “$” es EOF- marker </li></ul><ul><li>E  E+T E  E-T E  T </li></ul><ul><li>T-->T*F T  T/F T  F </li></ul><ul><li>F  id F  num F  (E) </li></ul><ul><li>Esta gramática acepta el mismo lenguaje que la </li></ul><ul><li>de sección 4.1 pero solo produce un árbol de </li></ul><ul><li>parsing por oración. Esta gramática no podría producir </li></ul><ul><li>árboles como los siguientes: </li></ul>X + ?Y + ?U ?V * +
  31. 31. 4.4 Analizador Sintáctico <ul><li>En esta fase se analiza la estructura de la frase del programa. </li></ul><ul><li>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. </li></ul><ul><li>También el parser llama rutinas del análisis semántico para revisar si el significado del programa es el correcto. </li></ul><ul><li>Por ultimo el parser genera código intermedio para los estatutos y expresiones si no se encontraron errores en ellos. </li></ul>
  32. 32. (cont.) <ul><li>Existen diferentes técnicas o métodos para realizar un análisis </li></ul><ul><li>sintáctico “Parsing”. Estas técnicas se dividen en dos tipos: </li></ul><ul><li>Descendentes </li></ul><ul><li>Ascendentes </li></ul><ul><li>Las técnicas descendentes realizan su análisis partiendo desde el </li></ul><ul><li>símbolo inicial de la gramática y realizando derivaciones hasta </li></ul><ul><li>llegar a producir las hojas o tokens. </li></ul><ul><li>Por otra parte las técnicas ascendentes realizan su análisis </li></ul><ul><li>partiendo de las hojas o tokens y mediante una serie de </li></ul><ul><li>operaciones llamadas reducciones terminan en la raíz o símbolo </li></ul><ul><li>inicial de la gramática. </li></ul><ul><li>Por lo general las técnicas descendentes son mas sencillas de </li></ul><ul><li>implementar que las ascendentes, pero por otra parte son menos </li></ul><ul><li>eficientes </li></ul>
  33. 33. 4.4.1 Analizador descendente (LL). <ul><li>Parsing Predictivo </li></ul><ul><li>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. </li></ul><ul><li>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. </li></ul>
  34. 34. (cont.) <ul><li>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: </li></ul><ul><li>S  if E then S else S </li></ul><ul><li>S  begin S L </li></ul><ul><li>S  print E </li></ul><ul><li>L  end </li></ul><ul><li>L  ; S L </li></ul><ul><li>E  num = num </li></ul><ul><li>Nuestro Parser tendría 3 métodos (uno por cada producción) </li></ul>
  35. 35. 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 ); }
  36. 36. (cont.) <ul><li>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”. </li></ul><ul><li>Cuando el parser solo necesita “mirar” el siguiente token para hacer su función (llokahead(1)), recibe el nombre de Parser LL(1). </li></ul><ul><li>Un parser podría necesitar “mirar” K tokens por adelantado para tomar desiciones. En este caso recibe el nombre de parser LL(K). </li></ul><ul><li>Ejemplo: (parte de una gramática) </li></ul><ul><li>IF-STM  if EXP then STM else STM </li></ul><ul><li>|  if EXP then STM </li></ul>
  37. 37. Eliminación de Recursión por la izquierda <ul><li>Suponer que queremos construír un Parser predictivo para la gramática de sección 4.3. </li></ul><ul><li>S  E$ E  E+T E  E-T E  T </li></ul><ul><li>T-->T*F T  T/F T  F </li></ul><ul><li>F  id F  num F  (E) </li></ul>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:
  38. 38. (cont.) <ul><li>En general, si tenemos producciones X  X  </li></ul><ul><li>y X   donde  no comience con X </li></ul><ul><li>podemos aplicar la siguiente transformación: </li></ul><ul><li>X  X   X    X’ </li></ul><ul><li>X  X   X    X’ </li></ul><ul><li>X    X’    X’ </li></ul><ul><li> X    X’    X’ </li></ul><ul><li> X’   </li></ul>
  39. 39. (cont.) <ul><li>Aplicando la transformación a la gramática anterior, obtenemos la nueva equivalente gramática (sin recursión por la izquierda): </li></ul><ul><li>S  E$ </li></ul><ul><li>E  T E’ E’  + T E’ E’  - T E’ E’   </li></ul><ul><li>T  F T’ T’  * F T’ T’  / F T’ T’   </li></ul><ul><li>F  id F  num F  (E) </li></ul>
  40. 40. Factorización por la izquierda <ul><li>Otro problema en una gramática ocurre cuando dos producciones para el mismo no terminal comienza con el mismo símbolo. Por ejemplo: </li></ul><ul><li>IF-STM  if EXP then STM else STM </li></ul><ul><li>IF-STM  if EXP then STM </li></ul><ul><li>En dicho caso podemos factorizar por la izquierda la gramática. El resultado sería el sig: </li></ul><ul><li>IF-STM  if EXP then STM X </li></ul><ul><li>X   </li></ul><ul><li>X  else IF-STM </li></ul><ul><li>las producciones anteriores facilitarán el trabajo del parser predictivo. </li></ul>
  41. 41. 4.4.2 Analizador ascendente(LR y LALR). <ul><li>La debilidad de las técnicas descendentes LL(k) es que deben predecir que producciones usar, después de haber mirado los primeros k tokens de la cadena de entrada. </li></ul><ul><li>Una técnica ascendente mas poderosa es la técnica LR(K), la cual pospone la decisión hasta que ha visto los tokens de la entrada correspondientes a la parte derecha de la producción (además de k mas tokens también). </li></ul><ul><li>LR(K) quiere decir parser de izquierda a derecha, derivación por la derecha y “lookahead(k)”. Esta técnica fue introducida por primera vez en 1965 por Knuth. </li></ul>
  42. 42. (cont.) <ul><li>Un Parser LR(K) está compuesto de: </li></ul><ul><ul><li>La cadena de entrada </li></ul></ul><ul><ul><li>Una pila </li></ul></ul><ul><ul><li>Una tabla de Parsing LR </li></ul></ul><ul><li>El parser obtiene los tokens de la entrada y dependiendo de el token actual y de lo que está en el tope de la pila, ejecuta una de dos acciones (shift-reduce) con la ayuda de la tabla de parsing </li></ul>
  43. 43. Algoritmo de Parsing LR (aho,Sethi y Ullman) Tomar el primer token de w$ /* w es la cadena */ Repeat forever begin Sea s el estado en el tope de la pila y a el token actual; if acción[s,a] = shift s’ then begin push a’ primero y sedpués s’ al tope de la pila; obten el siguiente token de la cadena de entrada else if acción[s,a] = reduce A->B then begin pop 2*|B| símbolos fuera de la pila; Sea s’ ahora el estado en el tope de la pila; Push A y después goto[s’,A] al tope de la pila; Imprime la producción A->B end else if acción[s,a] = accept then return else error() end
  44. 44. Ejemplo: Parser LR(K) S4 s7 g2 s3 a S4 s7 g5 s6 r1 r1 r1 S20 s10 s8 g11 s9 S4 s7 g12 TABLA DE PARSING 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 id num print ; , + := ( ) $ S E L S20 s10 g15 g14 r5 r5 r5 r5 r5 r2 r2 s16 r2 s3 s18 r3 r3 r3 s19 s13 r8 r8 S20 s10 s8 g17 r6 r6 s16 r6 r6 S20 s10 s8 g21 S20 s10 s8 g23 r4 r4 r4 r4 r4 s22 r7 r7 r7 r7 r7 r9 s16 r9
  45. 45. Ejemplo (cont.): PILA ENTRADA ACCION <ul><li>a:=7;B:=c+(d:=5+6,d)$ shift </li></ul>1 id4 := 7;B:=c+(d:=5+6,d)$ shift 1 id4 := 6 7;B:=c+(d:=5+6,d)$ shift 1 id4 := 6 num10 ;B:=c+(d:=5+6,d)$ reduce E->num <ul><li>id4 := 6 E11 ;B:=c+(d:=5+6,d)$ reduce </li></ul><ul><li> S->id:=E </li></ul>. . . . . . . $ accept 1 S2 . . . . . . . . . . . . . .
  46. 46. Parsing LALR <ul><li>La técnica de parsing LALR (LookAhead-LR) evita el uso de tablas muy grandes, como las manejadas en la técnica de parsing LR. </li></ul><ul><li>Esta técnica fue inventada por DeRemer en 1971. </li></ul><ul><li>Casi cualquier construcción sináctica de un LP puede ser expresado de manera conveniente por una gramática LALR. </li></ul><ul><li>Generadores de Parsers famosos como YACC (Yet Another Compiler-Compiler-Johnson) producen un parser LALR. </li></ul><ul><li>Para una gramática de un LP como Pascal una tabla de parsing LALR ocuparía varios cientos de estados, mientras que una tabla LR serían miles de estados. </li></ul>
  47. 47. 4.5 Administración de tablas de símbolos. <ul><li>Como se estudió en la unidad anterior, durante el análisis de léxico se inicia la construcción de la tabla de símbolos. </li></ul><ul><li>Esto ocurre al detectarse las declaraciones en el programa fuente. </li></ul><ul><li>Sin embargo también el parser ayuda a realizar esta tarea pues el es quien llama a las respectivas rutinas semánticas para que realicen funciones relacionada con la tabla de símbolos ( ver figura ). </li></ul>
  48. 48. 4.6 Manejo de errores sintácticos y su recuperación. <ul><li>Dentro del código del parser predictivo estudiado en clase se puede observar que el parser llama un metodo “error” para el manejo de errores sintácticos, que es cuando el parser obtiene un token no esperado. </li></ul><ul><li>¿Cómo se maneja el error para esta clase de parsers? Una forma simple es ejecutar una excepción y parar la compilación. Esto como que no es muy amigable par el usuario. </li></ul><ul><li>La mejor técnica es desplegar un mensaje de error y recuperarse de este, para que otros posibles errores sintácticos puedan ser encontrados en la misma compilación. </li></ul>
  49. 49. (cont.) <ul><li>Un error sintáctico ocurre cuando la cadena de tokens de entrada no es una oración en el lenguaje. La recuperación del error es una forma de encontrar alguna oración correcta, similar a la cadena de tokens. </li></ul><ul><li>Esto se puede realizar por medio de borrar, reemplazar o insertar tokens. </li></ul><ul><li>Por ejemplo la recuperación de un error en S, podría ser al insertar un token if,begin o print (o pretender que existe en la cadena), desplegar el mensaje del error y continuar la compilación. </li></ul>
  50. 50. Ejemplo: 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: print(“se esperaba if, begin o print”); }} Un problema que puede ocurrir al insertar un token faltante es que el programa caiga en un ciclo infinito, por eso a veces es preferible y mas seguro borrar el token, ya que el ciclo terminará cuando el EOF sea encontrado. Esta técnica trabaja muy cercanamente con la tabla de parsing (cuando el parser se implementa con tablas). En un parser del tipo LR o LALR, la tabla de parsing tiene 4 acciones: shift, reduce, accept y error (entrada nula). Cuando el parser encuentra una acción error , se para el proceso de análisis y se reporta la falla.
  51. 51. 4.7 Generadores de código para analizadores sintácticos <ul><li>Como se vio al final del capítulo 3, Javacc es un generador de scanners y de parsers (https://javacc.dev.java.net/). </li></ul><ul><li>Javacc produce un parser del tipo descendente (recursivo) o LL(k) donde k (lookahead) puede ser cualquier número entero positivo. </li></ul><ul><li>El parser producido puede correr en cualquier plataforma que cuente con el compilador de Java. </li></ul><ul><li>En el ejemplo siguiente del uso de Javacc, utilizamos de nuevo la gramática presentada anteriormente (sección 4.4.1) para un parser predictivo. Ejecutamos Javacc con la anterior gramática y obtendremos un parser recursivo descendente para dicha gramática. </li></ul>
  52. 52. Ejemplo: gramática para estatutos “begin”, “if” y “print” PARSER_BEGIN(MiniParser) public class MiniParser { public static void main(String[] args) { MiniParser parser; try { // RGC: added line if( args.length == 0 ) parser = new MiniParser(System.in); // RGC: added lines else parser= new MiniParser ( new java.io.FileInputStream( args[0] ) ); } // RGC: End parser.Program(); } catch (ParseException e) { System.out.println(e.getMessage()); } //RGC: added lines catch( Exception e ) { System.out.println(e.getMessage()); } //RGC :End } } PARSER_END(MiniParser) SKIP : { &quot; &quot; | &quot;t&quot; | &quot;n&quot; | &quot;r&quot; }
  53. 53. (cont. Ejemplo) TOKEN : { <INT: &quot;INT&quot;> | <IF: &quot;if&quot;> | <THEN: &quot;then&quot;> | <ELSE: &quot;else&quot;> | <BEGIN: &quot;begin&quot;> | <PRINT: &quot;print&quot;> | <END: &quot;end&quot;> | <SEMI: &quot;;&quot;> | <EQUAL: &quot;=&quot;> | <ID: ([&quot;a&quot;-&quot;z&quot;]|[&quot;A&quot;-&quot;Z&quot;]) ([&quot;a&quot;-&quot;z&quot;]|[&quot;A&quot;-&quot;Z&quot;]|[&quot;0&quot;-&quot;9&quot;])* > } void Program() : {} { S() <EOF> } void S() : {} { <BEGIN> S() L() | <PRINT> E() | LOOKAHEAD(12) &quot;if&quot; E() &quot;then&quot; S() &quot;else&quot; S() | &quot;if&quot; E() &quot;then&quot; S() } void L() : {} { <END> | <SEMI> S() L() } void E() : {} { <ID> <EQUAL> <ID> }
  54. 54. Unidad V Análisis Semántico
  55. 55. 5.1 Analizador semántico <ul><li>Un compilador no solo tiene que revisar la sintaxis de código fuente, si no también la semántica de este. </li></ul><ul><li>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. </li></ul><ul><li>Por ejemplo un error semántico es usar (en pascal ó java) un identificador que no fue anteriormente declarado. </li></ul><ul><li>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. </li></ul>
  56. 56. 5.2 Verificación de tipos en expresiones. <ul><li>Cuando mezclamos diferentes tipos en una misma expresión o que llamamos una rutina que no existe existe un error semántico. </li></ul><ul><li>Una de las funciones del analizador smántico es verificar que los tipos de una expresión sean compatibles entre si. </li></ul><ul><li>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. </li></ul>
  57. 57. 5.3 Conversión de tipos. <ul><li>Algunas veces los tipos de una expresión o estatuto son diferente. </li></ul><ul><li>Por ejemplo en la asignación, </li></ul><ul><li>a = b * c; </li></ul><ul><li>el tipo del resultado de evaluar b*c es diferente al de el identificador a . </li></ul><ul><li>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). </li></ul><ul><li>Ejemplo: </li></ul><ul><li>float dinero; </li></ul><ul><li>int cambio; </li></ul><ul><li>dinero = (float) cambio; </li></ul>
  58. 58. <ul><li>En un parser recursivo-descendente, el código de las acciones semánticas es mezclado dentro del flujo de control de las acciones del parser. En un parser especificado en javaCC, las acciones semánticas son fragmentos de código de programa en java unido a las producciones gramáticales. </li></ul><ul><li>Cada símbolo terminal y noterminal puede asociarse con su propio tipo de valor semántico. </li></ul><ul><li>Por ejemplo en la siguiente gramática para YACC de una calculadora simple, el tipo asociado con exp e INT podría ser int : </li></ul><ul><li>%token INT PLUS MINUS TIMES UMINUS </li></ul><ul><li>%start exp </li></ul><ul><li>%left PLUS MINUS </li></ul><ul><li>%left TIMES </li></ul><ul><li>%left UMINIS </li></ul><ul><li>exp: INT | exp PLUS exp | exp MINUS exp | exp TIMES exp 1 </li></ul><ul><li>MINUS exp %prec UMINUS </li></ul>5.4 Acciones agregadas en un Analizador sintáctico descendente (top-down).
  59. 59. (cont.) <ul><li>Los otros tokens no necesitarían tener un valor. </li></ul><ul><li>Por otra parte el tipo asociado a un token debe por supuesto coincidir con el tipo de token que el scanner retorne. </li></ul><ul><li>Para una regla A  BCD, la acción semántica debe retornar un valor cuyo tipo es el asociado al noterminal A. Pero puede construír este valor de los valores asociados a los terminales y noterminales B, C, D. </li></ul>
  60. 60. Recursivo-descendente <ul><li>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. </li></ul><ul><li>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. </li></ul><ul><li>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): </li></ul>
  61. 61. (cont.) S  E$ E  T E’ E’  + T E’ E’  - T E’ E’   T  F T’ T’  * F T’ T’  / F T’ T’   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.
  62. 62. Interprete class Token2 { int kind; Object val; Token2(int k, Object v) { kind=k; val=v; } } final int EOF=0, ID=1, NUM=2, PLUS=3, MINUS=4,LPAREN=5, RPAREN=6, TIMES=7; int lookup(String id) { …. } int F_follow[] = {PLUS,TIMES,RPAREN,EOF}; int F() {switch(tok.kind) { case ID: int i=lookup((String)(tok.val));advance()return i ; case NUM: int i=((integer)(tok.val)).intVal(); advance(); return i ; case LPAREN: eat(LPAREN); int i=E(); eatOrSkipTo(RPAREN,F_follow); return i ; case EOF: default: print(&quot;1 esperaba ID,NUM, o parent izq&quot;); //skipto(F_follow); return 0; }} int T_follow[]= {PLUS,RPAREN,EOF}; int T() {switch(tok.kind) { case ID: case NUM: case LPAREN: return Tprime(F()); default: print(&quot;2 esperaba ID, NUM o parent izq&quot;); //skipto(T_follow); return 0; }} int Tprime(int a) {switch (tok.kind) { case TIMES: eat(TIMES); return Tprime(a*F()); case PLUS: case RPAREN: case EOF: return a ; default: print(&quot;3 esperaba ID, NUM o parent izq&quot;); //skipto(T_follow); return 0; }} void eatOrSkipTo(int expected, int[] stop) { if (tok.kind==expected) eat(expected); else {print(&quot;4 esperaba ID, NUM o parent izq&quot;); //skipto(stop);} } Acciones semánticas
  63. 63. Parser Automáticamente generado <ul><li>Una especificación del parser para javaCC consistiría de un conjunto de reglas gramaticales, cada una anotada o agregada con una acción semántica el cual sería un estatuto java. </li></ul><ul><li>Ejemplo: </li></ul>void Start(): { int i; } { i=Exp() <EOF> {System.Out.println(i);} } Int Exp(): { int a,i; } { a=Term() ( “+” i=Term() {a=a+i;} | “-” i=Term() {a=a-i;} )* { return a; } } Int Term(): { int a,i; } { a=factor() ( “*” i=Factor() { a=a*i;} | “/” i=Factor() {a=a/i;} )* { return a; } } Int Factor(): { Token t; int i; } { t=<IDENTIFIER> { return lookup(t.image); } | t=<INTEGER_LITERAL> {return Integer.parseInt(t.image); } | “(“ i=Exp() “)” {return i; } }
  64. 64. Árboles de Parsing Abstractos <ul><li>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). </li></ul><ul><li>Una forma de hacerlo es producir un árbol de sintaxis abstracta (una forma condensada de árbol de parsing útil para representar construcciones del LP). </li></ul><ul><li>Por ejemplo la producción S  if B then S1 else S2 pudiera aparecer en un arbol sintáctico como: </li></ul>If-then-else 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. Est-if If Exp ( Exp ) Est Árbol De parsing Árbol sintáctico
  65. 65. (cont.) <ul><li>Otro ejemplo es en el árbol de parsing: </li></ul>Cuyo árbol sintáctico abstracto sería: + * 8 2 4 L E + T T * F F F 4 8 2
  66. 66. Ejemplo: <ul><li>La gramática siguiente nos muestra una sintaxis abstracta de un lenguaje para expresiones: </li></ul><ul><li>E  E+E E  E-E E  E*E </li></ul><ul><li>E  E/E E  id E  num </li></ul><ul><li>Esta gramática es impráctica para un parser ya que es ambigua pues no tiene precedencia de operadores. </li></ul><ul><li>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. </li></ul>
  67. 67. Árboles de Sintaxis en Java <ul><li>En Java las estructuras de datos para el árbol de sintaxis contienen una clase abstracta para cada noterminal y una subclase para cada producción. Así, las clases de el programa siguiente son las clases de la sintaxis abstracta para la gramática de la diapositiva anterior. </li></ul>
  68. 68. Programa de clases para Exp public abstract class ExpCh4 { public abstract int eval(); } class PlusExp extends ExpCh4 { private ExpCh4 e1,e2; public PlusExp(ExpCh4 a1, ExpCh4 a2) {e1=a1; e2=a2;} public int eval() { return e1.eval()+e2.eval(); } } class MinusExp extends ExpCh4 { private ExpCh4 e1,e2; public MinusExp(ExpCh4 a1, ExpCh4 a2) {e1=a1; e2=a2;} public int eval() { return e1.eval()-e2.eval(); } } class TimesExp extends ExpCh4 { private ExpCh4 e1,e2; public TimesExp(ExpCh4 a1, ExpCh4 a2) {e1=a1; e2=a2;} public int eval() { return e1.eval()*e2.eval(); } } class DivideExp extends ExpCh4 { private ExpCh4 e1,e2; public DivideExp(ExpCh4 a1, ExpCh4 a2) {e1=a1; e2=a2;} public int eval() { return e1.eval()/e2.eval(); } } class Identifier extends ExpCh4 { private String f0; public Identifier(String n0) {f0=n0;} public int eval() { return (7); //return lookup(f0); } } class IntegerLiteral extends ExpCh4 { private String f0; public IntegerLiteral(String n0) {f0=n0;} public int eval() { return (4); //return Integer.parseInt(f0); } }
  69. 69. (cont.) <ul><li>Ahora veamos un intérprete para el lenguaje de expresiones de la gramática de sección 4.1.1. Por conveniencia la mostramos de nuevo. </li></ul>S  E$ E  T E ’ E’  + T E’ E’  - T E’ E’   T  F T’ T’  * F T’ T’  / F T’ T’   F  id F  num F  (E) <ul><li>Nuestro intérprete primero construye árboles sintácticos y después los interpreta. El siguiente código es el de la gramática JavaCC con acciones semánticas para interpretar (evaluar) y producir (construir) árboles sintácticos. Cada clase de nodos de árbol sintáctico contiene una función eval que cuando es llamada retorna el valor de la expresión representada. </li></ul>
  70. 70. Gramática con acciones semánticas para árboles sintácticos PARSER_BEGIN(InterSinTree) class InterSinTree {} PARSER_END(InterSinTree) TOKEN: { <#DIGIT: [&quot;0&quot;-&quot;9&quot;]> |<ID: [&quot;a&quot;-&quot;z&quot;] ([&quot;a&quot;-&quot;z&quot;]|<DIGIT>)*> |<INTEGER_LITERAL: (<DIGIT>)+> } SKIP: { <&quot;--&quot; ([&quot;a&quot; - &quot;z&quot;])* (&quot;n&quot; | &quot;r&quot; | &quot;rn&quot;)> |&quot; &quot; |&quot;t&quot; |&quot;n&quot; |&quot;r&quot; } ExpCh4 Start(): { ExpCh4 e; } { e=Exp() <EOF> {System.out.println(e.eval()); return e; } } ExpCh4 Exp(): { ExpCh4 e1,e2; } { e1=Term() (&quot;+&quot; e2=Term() { e1=new PlusExp(e1,e2);} |&quot;-&quot; e2=Term() { e1=new MinusExp(e1,e2);} )* { return e1;} } ExpCh4 Term(): { ExpCh4 e1,e2; } { e1=Factor() (&quot;*&quot; e2=Factor() { e1=new TimesExp(e1,e2);} |&quot;/&quot; e2=Factor() { e1=new DivideExp(e1,e2);} )* { return e1;} } ExpCh4 Factor() : { Token t; ExpCh4 e; } { (t=<ID> {return new Identifier(t.image); } | t=<INTEGER_LITERAL> {return new IntegerLiteral(t.image); } | &quot;(&quot; e=Exp() &quot;)&quot; {return e; }) }
  71. 71. VISITADORES <ul><li>Es una técnica de patrones (opuesta a la orientada a objetos) que se puede usar para implementar el árbol sintáctico del compilador o intérprete. Un visitador es un objeto que contiene un método visit por cada clase de árbol sintáctico. Cada clase de árbol sintáctico debe contener un método accept . Cada método accept sirve como enganche para cada diferente tipo de operación sobre el árbol y es llamado por un visitador donde tiene una tarea: pasar el control de ida y venida (back and forth) entre el visitador y las clases del árbol sintáctico. </li></ul>
  72. 72. (cont.) <ul><li>A continuación veremos un ejemplo del intérprete de expresiones anterior pero ahora implementado con visitadores. Cada visitador implementa la interfase Visitor . Cada método accept toma un visitador como argumento y cada método visit toma un objeto de un nodo del árbol sintáctico como argumento. </li></ul>
  73. 73. Sintáxis Abstracta para MiniJava <ul><li>En la siguiente figura (siguiente diapositiva) mostramos las clases de la sintaxis abstracta para minijava. Solo los constructores son mostrados en la figura. Cada una de las clases de listas se implementa en la misma forma. Por ejemplo: </li></ul><ul><li>public class ExpList { </li></ul><ul><li>private Vector list; </li></ul><ul><li>public ExpList() { </li></ul><ul><li>list=new vector(); </li></ul><ul><li>} </li></ul><ul><li>Public void addElement (Exp n) { </li></ul><ul><li>list.addElement(n); </li></ul><ul><li>} </li></ul><ul><li>public Exp elementAt(int i) { </li></ul><ul><li>return (exp)list.elementAt(i); </li></ul><ul><li>} </li></ul><ul><li>public int size() { </li></ul><ul><li>return list.size(); </li></ul><ul><li>} </li></ul><ul><li>} </li></ul>
  74. 74. Package syntaxtree; Program (MainClass m, ClassDeclList cl) MainClass (Identifier i1, Identifier i2, Statement s) Abstract class ClassDecl ClassDeclSimple (Identifier i, VarDeclList vl, MethodDeclList ml) ClassDeclExtends (Identifier i, identifier j, VarDeclList vl, MethodDeclList ml) VarDecl (Type t, Identifier i) MethodDecl (Type t, Identifier i, FormalList fl, VarDeclList vl, StatementList, Exp e) Formal (Type t, Identifier i) Abstract class Type IntArrayType () BooleanType () IntegerType () IdentifierType (String s) Abstract class Statement Block (StatementList sl) If (Exp e, Statement s1, Statement s2) While (Exp e, Statement s) Print (Exp e) Assign (Identifier i, Exp e) ArrayAssign (Identifier i, Exp e1, Exp e2)
  75. 75. (cont. figura) Abstract class Exp And (Exp e1, Exp e2) LessTha n(Exp e1, Exp e2) Plus (Exp e1, Exp e2) Minu s(Exp e1, Exp e2) Times (Exp e1, Exp e2) ArrayLoockup (Exp e1, Exp e2) ArrayLength (Exp e) Call (Exp e, Identifier i, ExpList el) IntegerLiteral (int i) True () False () IdentifierExp (String s) This () NewArray (Exp e) NewObject (Identifier i) Not (Exp e) Identifier (String s) List classes ClassDeclList () ExpList () FormalList () MethodDeclList () StatementList () VarDeclList ()
  76. 76. Arbol Sintáctico <ul><li>Cada una de las clases que no son listas tiene un método accept para usarse con el patrón visitador. La interface Visitador se muestra en la siguiente diapositiva. </li></ul>
  77. 77. <ul><li>public interface Visitor { </li></ul><ul><ul><li>public void visit(Program n); </li></ul></ul><ul><ul><li>public void visit(MainClass n); </li></ul></ul><ul><ul><li>public void visit(ClassDeclSimple n); </li></ul></ul><ul><ul><li>public void visit(ClassDeclextends n); </li></ul></ul><ul><ul><li>public void visit(VarDecl n); </li></ul></ul><ul><ul><li>public void visit(MethodDecl n); </li></ul></ul><ul><ul><li>public void visit(Formal n); </li></ul></ul><ul><ul><li>public void visit(IntArrayType n); </li></ul></ul><ul><ul><li>public void visit(BooleanType n); </li></ul></ul><ul><ul><li>public void visit(IntegerType n); </li></ul></ul><ul><ul><li>public void visit(IdentifierType n); </li></ul></ul><ul><ul><li>public void visit(Block n); </li></ul></ul><ul><ul><li>public void visit(If n); </li></ul></ul><ul><ul><li>public void visit(While n); </li></ul></ul><ul><ul><li>public void visit(Print n); </li></ul></ul><ul><ul><li>public void visit(Assign n); </li></ul></ul><ul><ul><li>public void visit(ArrayAssign n); </li></ul></ul><ul><ul><li>public void visit(And n); </li></ul></ul><ul><ul><li>public void visit(LessThan n); </li></ul></ul><ul><ul><li>public void visit(Pluss n); </li></ul></ul><ul><ul><li>public void visit(Minus n); </li></ul></ul><ul><ul><li>public void visit(Times n); </li></ul></ul><ul><ul><li>public void visit(ArrayLoockup n); </li></ul></ul><ul><ul><li>public void visit(ArrayLength n); </li></ul></ul><ul><ul><li>public void visit(Call n); </li></ul></ul><ul><ul><li>public void visit(IntegerLiteral n); </li></ul></ul><ul><ul><li>public void visit(True n); </li></ul></ul><ul><ul><li>public void visit(False n); </li></ul></ul><ul><ul><li>public void visit(IdentifierExp n); </li></ul></ul><ul><ul><li>public void visit(This n); </li></ul></ul><ul><ul><li>public void visit(NewArray n); </li></ul></ul><ul><ul><li>public void visit(NewObject n); </li></ul></ul><ul><ul><li>public void visit(Not n); </li></ul></ul><ul><ul><li>public void visit(Identifier n); </li></ul></ul><ul><li>} </li></ul>Visitador MiniJava
  78. 78. (cont. Arbol Sintáctico) <ul><li>Podemos construir un árbol sintáctico usando expresiones new anidadas. Por ejemplo el árbol sintáctico para el estatuto MiniJava: </li></ul><ul><li>x = y.m(1,4+5); </li></ul><ul><li>usaría el siguiente código: </li></ul><ul><li>ExpList el= new ExpList(); </li></ul><ul><li>el.addElement(new IntegerLiteral(1)); </li></ul><ul><li>el.addelement(new Plus(new IntegerLiteral(4), </li></ul><ul><li>new IntegerLiteral(5))); </li></ul><ul><li>Statement s = new Assign(new Identifier “x”), </li></ul><ul><li> new Call(new identifierExp(“y”), </li></ul><ul><li> new Identifier(“m”), </li></ul><ul><li>el)); </li></ul>
  79. 79. 5.5 Pila semántica en un analizador sintáctico ascendente (bottom-up). <ul><li>Como fue visto en el capitulo anterior (4), un parser ascendente utiliza durante el análisis una pila. En esta va guardando datos que le permiten ir haciendo las operaciones de reducción que necesita. </li></ul><ul><li>Para incorporar acciones semánticas como lo es construir el árbol sintáctico, es necesario incorporar a la pila del parser otra columna que guarde los atributos de los símbolos que se van analizando. </li></ul><ul><li>Estos atributos estarían ligados a la correspondiente producción en la tabla de parsing </li></ul><ul><li>(consultar sección 5.3 del libro de “Aho, Ullman, Sethi” para ver mas detalles de la implementación). </li></ul>
  80. 80. 5.6 Administración de la tabla de símbolos <ul><li>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. </li></ul><ul><li>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. </li></ul><ul><li>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 . </li></ul>
  81. 81. (cont.) <ul><li>Un ambiente es un conjunto de atados (bindings) denotados por  . Por ejemplo, podemos decir que el ambiente z0 contiene los atados {g  string,a  int}, que significa que el identificador a es una variable entero y g es una variable string. </li></ul>
  82. 82. Ejemplo: <ul><li>Class C { </li></ul><ul><li>int a, int b; int c; </li></ul><ul><li>public void m() { </li></ul><ul><li>System.out.println(a+c); </li></ul><ul><li>int j=a+b; </li></ul><ul><li>String a=“hello”; </li></ul><ul><li>System.out.println(a); </li></ul><ul><li>System.out.println(j); </li></ul><ul><li>System.out.println(b); </li></ul><ul><li>} </li></ul><ul><li>} </li></ul>Suponer que compilamos esta clase en el ambiente z0 . Las declaraciones de campo en línea 2 nos da la tabla z1 igual a z0 + {a  int,b  int,c  int}. Los identificadores en línea 4 pueden encontrarse (look up) en ambiente z1 . En línea 5, la tabla o ambiente z2 = z1 +{j  int} es creada; y en línea 6, z3 = z2 +{a  string} es creada.
  83. 83. Implementación de la Tabla <ul><li>Existen dos opciones: El estilo funcional donde cuando z1 existe y z2 es creado, z1 sigue existiendo. Y el imperativo en donde z1 es destruido al crearse z2 . Mientras z2 existe no podemos mirar z1 . Pero al morir z2, z1 de nuevo existe. </li></ul>
  84. 84. Múltiple Tablas de Símbolos <ul><li>En algunos LP pueden existir varios ambientes a la vez: Cada módulo, o clase o registro en el programa tiene una tabla de símbolos z propia. Ejemplos (ML y Java). </li></ul>Structure M = struct structure E = struct val a= 5; end structure N = struct val b=10 val a=E.a+b end structure D = struct val d=E.a+N.a end end Package M; class E { static int a=5; } Class N { static int b=10; static int a=E.a+b; } Class D { static int d=E.a+N.a; }
  85. 85. (cont.) <ul><li>Al anlizar los 2 programas anteriores, sea z0 el ambiente base conteniendo funciones predefinidas, y sea </li></ul><ul><li>z1={a  int} </li></ul><ul><li>z2={E  z1} </li></ul><ul><li>z3={b  int,a  int} </li></ul><ul><li>z4={N  z3} </li></ul><ul><li>z5={d  int} </li></ul><ul><li>z6={D  z5} </li></ul><ul><li>z7=z2+z4+z6 </li></ul>
  86. 86. (cont.) <ul><li>En ML, N es compilado usando el ambiente z0+z2 para buscar los identificadores en la tabla;D es compilado usando z0+z2+z4 y el resultado del análisis es {M  z7}. </li></ul><ul><li>En Java, referencias adelantads son permitidas (dentro de N la expresión D.d sería legal), asi E,N y D son compilados en el ambiente z7. </li></ul>
  87. 87. TABLAS DE SIMBOLOS EN LENGUAJES IMPERATIVOS <ul><li>Un programa grande puede contener miles de distintos identificadores. Esto hace que la búsqueda en la tabla (loock up) tenga que ser eficiente. </li></ul><ul><li>En ambientes imperativos usualmente se usan tablas de dispersión. La operación z’=z +{a  t } es implementada insertando t en la tabla de dispersión usando la llave a . Una tabla de dispersión con encadenamiento externo funciona bien y soporta eliminación fácil de a  t para recuperar z al final del ambito de a . El siguiente programa implementa una tabla de dispersión. El “bucket i” es una lista ligada de todos los elementos cuya llave genere “ i mod SIZE ”. </li></ul>
  88. 88. (cont.) <ul><li>Considere z +{ a  t2 } cuando z ya contiene a  t1 . La función insert deja a  t1 en el “bucket” y pone a  t2 antes en la lista. Entonces, cuando pop se realiza después del ambito de a , z es restaurado. </li></ul>
  89. 89. SIMBOLOS <ul><li>Para evitar comparaciones innecesarias de cadenas podemos convertir cada cadena a un símbolo , y así todas las diferentes ocurrencias de cualquier cadena se conviertan a un mismo objeto símbolo. </li></ul><ul><li>El módulo símbolo implementa los símbolos y tiene estas propiedades: </li></ul><ul><ul><li>Comparar símbolos por igualdad o por mayor es rápido (comparación por apuntador o por entero). </li></ul></ul><ul><ul><li>Extraer una llave “hash” de tipo entero es rápido </li></ul></ul>
  90. 90. (cont.) <ul><li>Los ambientes son implementados en la clase Symbol.Table como Tables mapeando Symbols a ligados ( bindings ). </li></ul><ul><li>Para eso se manejan ligaduras para diferentes propósitos en el compilador – ligadura para tipos, para variables, para funciones, etc. </li></ul><ul><li>Entonces, una ligadura es un Objeto . </li></ul><ul><li>Para implementar la clase Symbol , usaremos el método intern() (java.lang.String), para darnos un objeto único a partir de una cadena de caracteres. </li></ul><ul><li>Para el uso de la tabla de símbolos usaremos java.util.Hashtable . La función beginScope “recuerda” el estado actual de la tabla y endScope restaura la tabla a donde estaba en el mas reciente beginScope que no ha terminado. </li></ul>
  91. 91. (cont.) <ul><li>Cuando la atadura x  b es metido a la tabla (table.put(x,b)), x es dispersado a un índice i, y un objeto “binder” x  b es puesto en la cabeza de la lista ligada para el bucket i . </li></ul><ul><li>Si la tabla ya tiene una ligadura x  b’, esto permanecería en el bucket, pero escondido por x  b . Esto es importante ya que soportaría la implementación de undo ( beginScope y endScope ). </li></ul><ul><li>También deberá existir una pila auxiliar, que muestre en que orden los símbolos son metidos (pushed) a la tabla de símbolos. Cuando x  b es encontrado, entonces x es metido a la pila (beginScope). Entonces, para implementar endScope, los símbolos deben sacarse de la pila. </li></ul>
  92. 92. Chequeo de Tipos en MiniJava <ul><li>¿Con que se llena una tabla de símbolos? Esto es, ¿Qué es la ligadura o “binding”? </li></ul><ul><li>Para realizar el chequeo de tipos de programas MiniJava, la tabla de símbolos debe contener toda la información declarada: </li></ul><ul><ul><li>Cada nombre de variable y nombre de parámetro formal debe ser ligado a su tipo. </li></ul></ul><ul><ul><li>Cada nombre de método debe ser ligado a sus parámetros, tipo de resultado y variables locales. </li></ul></ul><ul><ul><li>Cada nombre de clase debe ser ligado a su variable y declaraciones de métodos. </li></ul></ul><ul><ul><li>Liga a página con mas información. </li></ul></ul>
  93. 93. (cont.) <ul><li>Por ejemplo, considere la siguiente figura, que muestra un programa y su tabla de símbolos. </li></ul>Class B { C f; int [ ] j; int q; public int start(int p, int q) { int ret; int a; /* …… */ return ret; } public boolean stop(int p) { /* ….. */ return false; } } Class C { /* ….*/ } B C FIELDS f C j int [ ] q int METHODS start int stop bool PARAMS p int q int LOCALS ret int a int PARAMS p int LOCALS ……
  94. 94. (cont.) <ul><li>Los tipos primitivos en MiniJava son int y boolean; todos los otros tipos son arreglo de enteros o nombres de clases. </li></ul><ul><li>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”. </li></ul><ul><li>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. </li></ul>
  95. 95. (cont.) <ul><li>La primera fase de el checador de tipos se puede implementarse por medio de un visitador que visite los nodos del árbol sintáctico de MiniJava y construya la tabla de símbolos. </li></ul><ul><li>Por ejemplo el método visitador en el siguiente programa maneja declaraciones de variables. Este agrega el nombre de la variable y el tipo a la estructura de datos para la clase actual que mas tarde será agregada a la tabla de símbolos. </li></ul><ul><li>El método visitador checa si la variable ya fue declarada anteriormente. </li></ul>
  96. 96. Método Visitador Class ErrorMsg { boolean anyErrors; void complain (String msg) { anyErrors = true; System.out.println(msg); } } // Type t; // Identifier i; Public void visit(VarDecl n) { Type t = n.t.accept(this); String id= n.i.toString(); if (currMethod ==null) { if (!currClass.addVar(id,t)) error.complain(id + “is already defined in “ + currClass.getId()); } else if (!currentMethod.addVar(id,t)) error.Complain(id + “is already defined in “ + currClass.getId( ) + “.” + currMethod.getId( ));
  97. 97. (cont.) <ul><li>La segunda fase del checador de tipos puede ser implementada por un visitador que cheque tipos de todas los estatutos y expresiones. </li></ul><ul><li>El tipo del resultado de cada método visitador es String , que representa los tipos de MiniJava. </li></ul><ul><li>La idea es que cuando el visitador visita una expresión, entonces retorna el tipo de esa expresión. </li></ul><ul><li>El siguiente método (siguiente diapositiva) es el visitador que implementa la adición (plus) e1 + e2 . En MiniJava ambos operandos deben ser de tipo entero (el checador revisa esto) y el resultado debe ser entero (el checador retorna este tipo). </li></ul>
  98. 98. Método Visitador para expresiones Plus // Exp e1, e2; Public Type visit(Plus n) { if (! (n.e1.accept(this) instanceOf IntegerType) ) error.complain(“Left side of LessThan must be of type integer”); if (! (n.e2.accept(this) instanceOf IntegerType) ) error.complain(“Right side of LessThan must be of type integer”); return new IntegerType( ); }
  99. 99. 5.7 Manejo de errores semánticos. <ul><li>Cuando el checador de tipos detecta un error de tipos o un identificador no declarado, debe imprimir el mensaje de error y continuar. </li></ul><ul><li>Esto debido a que normalmente el programador prefiere que le describan todos los errores posibles del programa fuente. </li></ul><ul><li>Esto quiere decir, que si un error de tipos es encontrado, no debe producirse un programa objeto por parte del compilador. </li></ul><ul><li>Así, las siguientes fases no deben ejecutarse. </li></ul><ul><li>Hasta esta etapa (chequeo de tipos), la parte del compilador se conoce con el nombre de “front End”. </li></ul>
  100. 100. REGISTROS DE ACTIVACION <ul><li>En casi cualquier LP, una función (método) puede tener variables locales que son creadas cuando se llama la función (al entrar a esta). </li></ul><ul><li>Diferentes invocaciones a la función pueden existir a la vez, y cada invocación tiene su propia “instanciación” de variables. </li></ul>
  101. 101. (cont.) <ul><li>En el siguiente método de Java </li></ul>Int f(int x) { int y= x+x; if (y<10) return f(y); else return y-1; Una nueva instancia de x es creada (e inicializada por el llamador de “f”) cada vez que “f” es llamada. Debido a que existen llamadas recursivas, muchas de esas x existen simultáneamente. Similarmente, una nueva instancia de y es creada cada vez que el cuerpo f es iniciado.
  102. 102. (cont.) <ul><li>En muchos LP (incluyendo Pascal, C y java), las variables locales son destruidas cuando una función retorna. Ya que las variables locales son creadas y destruidas en una forma LIFO, podemos usar una pila para manejarlas. </li></ul>
  103. 103. MARCOS DE PILA <ul><li>Debido a que se trabaja con bloques de datos por función un “push” y “pop” no funciona. </li></ul><ul><li>Entonces la pila es tratada como si fuera un gran arreglo, con un registro especial- el “stack pointer” que apunta a una localidad. </li></ul><ul><li>Todas las localidades después del apuntador son basura y todas las que están antes están asignadas. </li></ul><ul><li>El área en la pila dedicada a las variables locales, parámetros, dirección de retorno y otras variables temporales para una función es llamada el registro de activación o marco de pila de la función. </li></ul>
  104. 104. (cont.) <ul><li>El diseño de la estructura de los marcos es de acuerdo con la arquitectura y el LP que se compila. </li></ul><ul><li>Aunque normalmente el constructor de la arquitectura define un diseño de marco standard para todos los compiladores para esa arquitectura. </li></ul>
  105. 105. Ejemplo: Un marco de pila Argumentos de entrada Apuntador Del marco Argumentos De salida Apuntador De pila Argumento n … … Argumento 1 Liga estática Argumento m … … Argumento 1 Liga estática Variables locales Dirección retorno Temporales Registros salvados Marco actual Marco anterior Marco siguiente Direcciones de Memoria mas altas
  106. 106. Marco de Pila <ul><li>Los argumentos de entrada son los pasados por el llamador (técnicamente son parte del marco anterior pero pueden accesarse usando un desplazamiento del apuntador de marco). </li></ul><ul><li>Cuando la función actual llama otras funciones, puede usar el espacio de los argumentos de salida para pasar parámetros. </li></ul><ul><li>La dirección de retorno es creada por la instrucción CALL. </li></ul><ul><li>Las variables locales también tienen su espacio. </li></ul><ul><li>Las variables mantenidas en registros algunas veces son salvadas a memoria. </li></ul>
  107. 107. El Apuntador de Marco (FP) <ul><li>Suponer que una función g(…) llama la función f(a 1 ,…a n ). Diremos que g es el llamador (caller) y f el llamado (callee). Al entrar a f , el apuntador de la pila (SP) apunta al primer argumento que g pasa a f. Al entrar, f coloca un marco solo con restar el tamaño del marco de el SP. </li></ul><ul><li>El viejo SP se convierte en el actual FP y el viejo FP es salvado en el marco. </li></ul><ul><li>Cuando FP termina, solo copia FP de regreso a SP y regresa el valor viejo salvado de FP. </li></ul><ul><li>Si los marcos son siempre del mismo tamaño entonces no es necesario contar con FP y todo se simplifica sumando o restando la constante framesize a SP. </li></ul>
  108. 108. Registros <ul><li>Por eficiencia, es importante mantener las variables locales, resultados intermedios y otros valores en registros en lugar de la pila de marcos. </li></ul><ul><li>Si función f llama a g y ambas hacen uso de registro r , entonces r debe ser salvado (dentro de la pila de marcos) antes de que lo use g y restaurado (desde la pila) después de que termine g . </li></ul><ul><li>¿de quien es responsabilidad de salvar r ? ¿de f o g ? si lo salva f se dice que r es un registro caller-save; si lo salva g se llama callee-save. </li></ul>
  109. 109. Pase de Parámetros <ul><li>Estudios actuales han mostrado que raramente una función pasa mas de 4 parámetros. </li></ul><ul><li>Debido a esto, la mayoría de las máquinas definen que los primeros k argumentos (con k=4) se pasan en registros y el resto en memoria. </li></ul>
  110. 110. Direcciones de Retorno <ul><li>Si g llama a f , entonces si la instrucción call dentro de g está en dirección a , el lugar de retorno en g es a+1 , la siguiente instrucción del call . </li></ul><ul><li>En máquinas modernas la dirección de retorno es pasada a un registro en lugar de la memoria. </li></ul><ul><li>En funciones “hoja” la dirección no necesita ponerse en la pila. </li></ul>
  111. 111. Registros vs. Memoria <ul><li>Registros siempre deben usarse en asignación a menos que: </li></ul><ul><ul><li>La variable sea pasada por referencia </li></ul></ul><ul><ul><li>La variable es accesada por una función anidada dentro de la función actual. </li></ul></ul><ul><ul><li>El valor es demasiado grande para un registro. </li></ul></ul><ul><ul><li>La variable es un arreglo, donde es necesario realizar aritmética de direcciones. </li></ul></ul><ul><ul><li>El registro que contiene la variable es necesitado para otros propósitos. </li></ul></ul><ul><ul><li>Existen demasiadas variables locales y valores temporales </li></ul></ul>
  112. 112. Ligas Estáticas (Static Links) <ul><li>En LP que admiten funciones anidadas (Pascal,ML y Java) las funciones de mas adentro pueden usar variables declaradas en funciones de mas afuera (Estructuras de Bloque). </li></ul><ul><li>En el siguiente programa (sig. Diapositiva) la función write hace referencia a la variable de afuera output e indent hace referencia a n y output . Para cumplir con esto, indent debe tener acceso no solo a su propio marco (para i y s ) sino también a los marcos de show (por n ) y prettyprint (por output ). </li></ul>
  113. 113. Programa de funciones Anidadas Type tree= {key: string, left: tree, right: tree} Function prettyprint(tree:tree): string= let var output := “ “ function write(s:string) = output :=concat(output,s) function show(n:int, t:tree) = let function indent(s:string)= (for i:= 1 to n do write(“ “); output:=concat(output,s);write(“ “); in if t=nil then indent(“.”) else (indent(t.key); show(n+1,t.left); show(n+1,t.right)) end in show(0,tree); output end
  114. 114. Ligas Estáticas (cont.) <ul><li>Existen varios métodos para solucionar lo anterior: </li></ul><ul><ul><li>Siempre que una función f sea llamada, puede pasarse un apuntador a el marco de la función que estáticamente encierra a f ; este apuntador es la liga estática . </li></ul></ul><ul><ul><li>Un arreglo global puede mantenerse, conteniendo -en posición i - un apuntador a el marco del procedimiento mas recientemente activado cuyo profundidad de anidamiento estático es i . Este arreglo es llamado un “display”. </li></ul></ul><ul><li>A continuación describimos el método de liga estática para el ejemplo de la diapositiva anterior. </li></ul>
  115. 115. (cont.) <ul><li>Línea 21: prettyprint llama show , pasando el apuntador del marco del propio prettyprint como una liga estática de show . </li></ul><ul><li>Línea 10: show guarda su liga estática (la dirección del marco de prettyprint ) dentro de su propio marco. </li></ul><ul><li>Línea 15: show llama indent , pasando su propio apuntador de marco como liga estática de indent . </li></ul><ul><li>Línea 17: show llama a show ,pasando su propia liga estática (no su propio apuntador de marco) como la liga estática. </li></ul><ul><li>Línea 12: indent usa el valor n del marco de show . Para hacer esto, trae un desplazamiento apropiado de la liga estática de indent (que apunta al marco de show ). </li></ul><ul><li>Línea 13: indent llama a write . Debe pasar el apuntador de marco de prettyprinter como la liga estática. Para obtener esto, primero trae un desplazamiento de su propia liga estática (desde el marco de show ), la liga estática que había sido pasada a show . </li></ul><ul><li>Líea 14: indent usa la variable output del marco de prettyprint . Para hacer esto, comienza con su propia liga estática, entonces trae a show , y luego trae a output . </li></ul>
  116. 116. Unidad VI Generación de Código Intermedio
  117. 117. 6.1 Lenguajes intermedios. <ul><li>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. </li></ul><ul><ul><li>Código fuente             Código intermedio                Código Objeto </li></ul></ul><ul><li>Un compilador produce primero un código intermedio, pues producir directamente el código objeto resulta sumamente complicado e ineficiente. </li></ul>
  118. 118. (cont.) <ul><li>Ventajas de producir código intermedio: </li></ul><ul><li>Más fácil de producir código objeto después, si se parte de un código intermedio. </li></ul><ul><li>Facilita y hace eficiente  la optimización de código (algunos tipos de optimización). </li></ul><ul><li>El código intermedio es transportable (puede usarse en diferentes arquitecturas) ya que no hace referencia a componentes de la máquina. </li></ul>
  119. 119. 6.2 Notaciones. <ul><li>Infija. Es la notación habitual. El orden es primer operando, operador, segundo operando. </li></ul><ul><li>Ejemplo: a/b/c </li></ul><ul><li>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. </li></ul>
  120. 120. (cont.) <ul><li>Postfija. El orden es primer operando, segundo operando, operador. </li></ul><ul><li>Ejemplo : La expresión  X+Y-X*Y </li></ul><ul><li>en notación Postfija  es XY+XY*- </li></ul><ul><li>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. </li></ul><ul><li>Por ejemplo , suponer que X=1 y Y=2. </li></ul><ul><ul><ul><li>Las operaciones serían: </li></ul></ul></ul><ul><ul><ul><li>push 1 (meter 1) </li></ul></ul></ul><ul><ul><ul><li>push 2 </li></ul></ul></ul><ul><ul><ul><li>' + ' requiere de 2 operandos, se sacan, se suman y se mete el resultado (3) </li></ul></ul></ul><ul><ul><ul><li>push 1 </li></ul></ul></ul><ul><ul><ul><li>push 2 </li></ul></ul></ul><ul><ul><ul><li>' * ' se mete el resultado(2) </li></ul></ul></ul><ul><ul><ul><li>' - ' se mete el resultado (1) </li></ul></ul></ul><ul><li>Notación prefija : El orden es operador, primer operando, segundo operando. </li></ul>
  121. 121. 6.3 Representación de código intermedio. <ul><li>Existen muchas clases de representaciones </li></ul><ul><li>intermedias. Algunas de las mas comunes son: </li></ul><ul><ul><li>Notación polaca. </li></ul></ul><ul><ul><li>Código P (Pascal). </li></ul></ul><ul><ul><li>Triples y Cuadruples. </li></ul></ul><ul><ul><li>Bytecodes (Java) </li></ul></ul><ul><ul><li>MSIL (C#) </li></ul></ul>
  122. 122. Notación Polaca <ul><li>También conocida como notación postfija. Se utiliza como se dijo anteriormente en máquinas de Pila. </li></ul><ul><li>Ejemplos: </li></ul><ul><li>Pascal Notación Polaca </li></ul><ul><li>a+b-c ab+c- </li></ul><ul><li>a+b*c abc*+ </li></ul><ul><li>a+b*c+d abc*+d+ </li></ul>
  123. 123. Código P <ul><li>Se usó como código intermedio y objeto en las primeras implementaciones de Pascal. </li></ul><ul><li>El código P era interpretado en una máquina abstracta. </li></ul><ul><li>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”. </li></ul><ul><li>La máquina P está orientada a usarse en una pila (stack-oriented). </li></ul>
  124. 124. Ejemplo: <ul><li>Insn. Stack Stack Description </li></ul><ul><li>before after </li></ul><ul><li>adi i1 i2 i1+i2 add two integers </li></ul><ul><li>Adr r1 r2 r1+r2 add two reals </li></ul><ul><li>dvi i1 i2 i1/i2 integer division </li></ul><ul><li>ldci i1 i1 load integer constant </li></ul><ul><li>mov a1 a2 move </li></ul><ul><li>not b1 ~b1 boolean negation </li></ul>
  125. 125. Triples y Cuadruplos (Código de 3 Direcciones) <ul><li>Ha sido uno de los mas populares. Sus instrucciones constan de 3 direcciones o registros para 2 argumentos u operandos y el resultado. </li></ul><ul><li>Su formato es: </li></ul><ul><li>resultado:= argumento1  operador argumento2 </li></ul><ul><li>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. </li></ul>
  126. 126. (cont.) <ul><li>EJEMPLO:              Z:= X + Y – X * Y </li></ul><ul><li>ADD                   X          Y             VAR1 </li></ul><ul><li>MUL                   X          Y            VAR2 </li></ul><ul><li>SUB                  VAR1   VAR2         VAR3 </li></ul><ul><li>STORE             VAR3       Z </li></ul>
  127. 127. (cont.) <ul><li>EJEMPLO : (Estructure de Control): </li></ul><ul><li>If (a==b) </li></ul><ul><li>a=0; </li></ul><ul><li>else </li></ul><ul><li>a=1; </li></ul><ul><li>En cuadruplos tendríamos: </li></ul><ul><li>1 - A B t1 </li></ul><ul><ul><li>2 JnZ t1 5 </li></ul></ul><ul><ul><li>3 = 0 A </li></ul></ul><ul><ul><li>4 JP 6 </li></ul></ul><ul><ul><li>5 = 1 A </li></ul></ul><ul><ul><li>6 </li></ul></ul>
  128. 128. (cont.) <ul><li>En el código de 2 direcciones se evita el uso se variables temporales. El formato es: </li></ul><ul><li>Operador argumento1 argumento2 </li></ul><ul><li>EJEMPLO : Z = X + Y  - X  * Y  </li></ul><ul><ul><ul><li>ADD            X        Y       </li></ul></ul></ul><ul><ul><ul><li>MUL            X        Y </li></ul></ul></ul><ul><ul><ul><li>SUB            (1)     (2)     </li></ul></ul></ul><ul><ul><ul><li>STORE       (3)     (Z) </li></ul></ul></ul><ul><ul><ul><li>Donde los números entre paréntesis representan </li></ul></ul></ul><ul><ul><ul><li>apuntadores a la secuencia de operaciones de 2 direcciones. </li></ul></ul></ul>
  129. 129. Java Bytecodes <ul><li>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. </li></ul><ul><li>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. </li></ul><ul><li>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. </li></ul>
  130. 130. (cont.) <ul><li>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. </li></ul><ul><li>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. </li></ul><ul><li>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). </li></ul>
  131. 131. (cont.) <ul><li>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. </li></ul><ul><li>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. </li></ul><ul><li>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. </li></ul><ul><li>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. </li></ul>
  132. 132. La Máquina Virtual de Java (JVM) <ul><li>La siguiente liga nos lleva a la información de lo que es la JVM y mas sobre Java bytecodes . </li></ul>
  133. 133. Árboles de Ensamblador <ul><li>Un paso antes de generar Ensamblador Real </li></ul><ul><li>Permite mas fácil optimización de código </li></ul><ul><li>Primero vamos a estudiar la implementación de asignación de memoria en el código generado (stack y heap). </li></ul>
  134. 134. Implementación de variables <ul><li>En Java las variables locales son almacenadas en la pila y en registros. </li></ul><ul><li>Las variables de arreglos y de clases son almacenadas en el heap . </li></ul><ul><li>Registros de Activación (RA). </li></ul><ul><ul><li>Son los segmentos de la pila que contienen las variables para una función. También se le llama “stack frame”. También almacenan valores de registros (saved registers), retornos, parámetros, etc. </li></ul></ul>
  135. 135. Por ejemplo, la función: Int foo() { int a; int b; /* body of foo */ } FP SP Tendría el registro de activación Existen dos apuntadores. FP que apunta al inicio del RA actual y SP que apunta a la primera localidad vacía. El valor retornado de una función es puesto en un registro especial en lugar de la pila. Cuando una función es llamada los valores de los parámetros de entrada son puestos en la pila, y cuando la función retorna un valor, el valor es almacenado en el registro de resultado. Registros salvados b a
  136. 136. (cont.) <ul><li>Como el FP siempre apunta al comienzo del RA actual, podemos acceder a las variables locales usando un offset desde el FP. </li></ul><ul><li>Los parámetros de funciones son almacenados en RA de la función que llama, no la que es llamada. Los parámetros de entrada a una función pueden ser accesados por medio del FP, usando un offset en la dirección opuesta a las variables locales. El formato completo de un RA es mostrado en la siguiente diapositiva. </li></ul>
  137. 137. Formato completo de RA FP SP Previous Activation Record Current Activation Record Saved Registers Local Variables Input Parameter 1 Input Parameter 2 … Input Parameter n
  138. 138. Considere el siguiente programa: void foo(int a, int b); void foo(int c, int d); void main() { int u; int v; /* Label A */ bar(1,2); } void bar(int a, int b) { int w; int x; foo(3,4); } void foo(int c, int d) { int y; int z; /* Label B */ }
  139. 139. En label A de el programa, la pila de RA se miraría de la forma siguiente FP SP Activation Record for main La variable local u puede accesarse examinando la localidad de memoria apuntada por FP. La variable v puede accesarse examinando (FP-wordsize). Algo para aclarar es que la pila crece de direcciones altas a bajas. Saved Registers v u
  140. 140. En etiqueta B los RA se mirarían así: FP SP Activation Record for main Activation Record for bar Activation Record for foo Saved Registers z y c d Saved Registers x w a b Saved Registers v u
  141. 141. (cont.) <ul><li>La variable y en función foo puede accesarse al examinar la localidad de memoria apuntada por FP . La variable z en foo puede accesarse examinando la localidad (FP-wordsize). El parámetro de entrada c en función foo puede accesarse examinando la localidad (FP+wordsize), mientras que el parámetro d puede accesarse con (FP+2*wordsize). </li></ul><ul><li>Algo importante es que la función llamadora es responsable de poner los parámetros actuales y de quitarlos cuando acabe la función llamada. </li></ul>
  142. 142. Ensamblador Abstracto <ul><li>El ensamblador abstracto es dividido en árboles de estatutos y árboles de expresiones. Árboles de estatutos no tienen valores pero los de expresiones si lo tienen. </li></ul><ul><li>Existen cinco diferentes tipos de árboles de expresiones: </li></ul><ul><ul><li>Constante. Consiste de un valor de la constante (solo enteros). </li></ul></ul><ul><ul><li>Registro. Una cadena que especifica el registro. </li></ul></ul><ul><ul><li>Operador. Dos árboles de expresión para los operandos, y el operador: +,-,*,/,<,>,<=,>=,=,&&,||,!. </li></ul></ul>
  143. 143. (cont.) <ul><li>CallExpression. Contiene una etiqueta de lenguaje ensamblador y un árbol de expresión por cada uno de los parámetros en la llamada de la función (function call). La etiqueta es la dirección del inicio de la función llamada. Por ejemplo la función foo(3,4,5) se representaría por el árbol de expresión: </li></ul>Constant(3) Constant(5) El ensamblador abstracto en este caso no contiene instrucciones explícitas para asignar los parámetros actuales a la pila. Cuando se tradusca el ensamblador abstracto de una callExpression a ensamblador real, se incluye el código para hacer la copia de los parámetros actuales a la pila. CallExpresion(“foo”) Constant(4)
  144. 144. <ul><li>Memoria. Solo se denota la dirección de memoria. </li></ul><ul><li>Ejemplo: </li></ul>Casi nunca se manejan direcciones absolutas sino que estas son relativas al FP. Por ejemplo para referirse a la localidad de memoria de una variable local con un desplazamiento (oofset) de 4 del FP es representado con el árbol: Memory Constant(1006) Memory Operator(-) Register(FP) Constant(4)
  145. 145. <ul><li>Existen 8 tipos de árboles de estatutos: </li></ul><ul><ul><li>Move. Tienen dos subárboles – el´subárbol izquierdo es el destino del move, y el derecho es el valor a mover. El izquierdo necesita ser un árbol de expresión de registro (mover un dato a un registro) o un árbol de expresión de memoria (mover un dato a memoria). La parte derecha puede ser una expresión arbitraria. </li></ul></ul><ul><ul><li>Label (Etiqueta). Se usan como destinos de “jumps” y “calls”. </li></ul></ul><ul><ul><li>Jump. Saltos incondicionales contienen una etiqueta. </li></ul></ul><ul><ul><li>Conditional Jump. Contienen un subárbol de expresión y una etiqueta de lenguaje ensamblador. Si la expresión es verdadera se hace una transferencia de control a una etiqueta. Si es falsa, entonces habrá un no-op. </li></ul></ul><ul><ul><li>Sequential. Tiene dos subárboles. Representa la ejecución del izquierdo seguido del derecho. </li></ul></ul><ul><ul><li>CallStatement. Contienen una árbol de etiqueta, y un árbol expresión de cada uno de los parámetros actuales. “Calls” representan “void function calls”. </li></ul></ul><ul><ul><li>Empty. Son “no-ops” y son removidos cuando se traduce a ensamblador real. </li></ul></ul><ul><ul><li>Return. No retorna un valor (como Java). Solo cambia el flujo de control. Un “return” de Java se implementa incluyendo código que retorne el valor de la función (en el registro resultado) además del ensamblador abstracto del “return”. </li></ul></ul>
  146. 146. Ejemplos: (asumiremos que wordsize=4 y que un entero se guarda en una palabra) void foo(int a, int b) { int x; int y; boolean z; x = 1; y = a * b; y++; bar(y, x + 1, a); x = function(y+1, 3); if (x > 2) z = true; else z = false; }
  147. 147. X=1; Move Memory Constant(1) Register(FP)
  148. 148. y = a * b; Move Memory Operator (-) Register(FP) Constant(4) Operator(*) Memory Memory Operator (+) Operator (+) Register(FP) Constant(4) Register(FP) Constant(8)
  149. 149. Y++; Move Memory Operator (-) Constant(4) Operator(+) Memory Constant(1) Operator (-) Register(FP) Constant(4) Register(FP)
  150. 150. bar(y,x+1,a); CallStatement(“bar”) Memory Operator (-) Constant(4) Register(FP) Operator (+) Memory Constant(1) Register(FP) Memory Operator (+) Constant(4) Register(FP)
  151. 151. x=function(y+1,3); Move Memory Register(FP) Callexpression(“function”) Constant(3) Constant(1) Operator(+) Memory Operator(-) Register(FP) Constant(4)
  152. 152. if (x > 2) z = true; else z = false; Sequential ConditionalJump(“iftrue”) Operator(>) Memory Constant(2) Sequential Sequential Sequential Sequential Label(“ifend”) Jump(“ifend”) Label(“iftrue”) Move Move Constant(0) Memory Operator(-) Register(FP) Constant(8) Constant(1) Memory Operator(-) Register(FP) Constant(8) Register(FP)
  153. 153. Creación de Ensamblador Abstracto <ul><li>Variables </li></ul><ul><ul><li>Variables base </li></ul></ul><ul><ul><li>La variable x y y se representarían con los árboles: </li></ul></ul>void foo() { int x; int y; /* Body of foo */ } Memory Register(FP) Memory Operator(-) Register(FP) Constant(4)
  154. 154. <ul><ul><li>Variables Arreglo: Son almacenadas en el heap, y usando un apuntador a la base del arreglo el cual se almacena en la pila. </li></ul></ul><ul><ul><li>La siguiente función o método </li></ul></ul>void foo arrayallocation() { int x; int A[]; int B[]; A = new int [5]; B = new int [5]; /* Body of function */ }
  155. 155. La variable local x es almacenada en la pila, igual que la dirección base del arreglo A y del arreglo B. Stack Heap x A B FP SP A[0] A[1] A[2] A[3] A[4] B[0] B[1] B[2] B[3] B[4] Saved Registers
  156. 156. ¿Cómo debemos representar el arreglo A[3] Register(FP) Memory Operator(-) Memory Operator(*) Operator(-) Constant(WORDSIZE) Constant(WORDSIZE) Constant(3)
  157. 157. ¿ Y A[x] ? Memory Operator(-) Memory Operator(*) Operator(-) Register(FP) Constant(WORDSIZE) Constant(WORDSIZE) Memory Register(FP)
  158. 158. ¿ Y para A[B[2]] ? Memory Operator(-) Memory Operator(*) Operator(-) Register(FP) Constant(WORDSIZE) Constant(WORDSIZE) Memory Operator(-) Memory Operator(-) Register(FP) Constant(2*WORDSIZE) Operator(*) Constant(WORDSIZE) Constant(2)
  159. 159. Arreglos Multidimensionales se manejan de manera similar. Ejemplo: void twoDarray { int i; int c [ ] [ ] ; C = new int [3] [ ] ; for (i=0; i<3; i++) C[i] = new int[2] ; /* Body of function */ }
  160. 160. El RA y el Heap se ven así: Stack Heap C[0] C[1] C[2] C[0] [0] C[0] [1] C[1] [0] C[1] [1] C[2] [0] C[2] [1] i C FP SP Saved Registers
  161. 161. La variable C sería representada así: Memory Operator(-) Register(FP) Constant(WORDSIZE)
  162. 162. La variable C[2] así: Memory Operator(-) Memory Operator(*) Operator(-) Register(FP) Constant(WORDSIZE) Constant(WORDSIZE) Constant(2)
  163. 163. La variable c[2] [1] Memory Operator(-) Memory Operator(*) Operator(-) Memory Operator(*) Constant(WORDSIZE) Constant(1) Operator(-) Register(FP) Constant(WORDSIZE) Constant(2) Constant(WORDSIZE)
  164. 164. Variables Instanciadas Son muy similares a las variables arreglo. La única diferencia es que en las variables arreglo, el desplazamiento para el índice necesita ser calculado, mientras que en las variables instanciadas, el desplazamiento es conocido en tiempo de compilación. Ejemplo: class simpleClass { int x; int y; int A[ ]; } void main() { simpleClass(); s = new simpleClass(); s.A = new int[3] /* Body of main */ }
  165. 165. ¿Cuál es el árbol de ensamblador para s.y ? Memory Register(FP) Él árbol de s : Para obtener y de s : Memory Memory Operator(-) Constant(WORDSIZE) Register(FP)
  166. 166. El árbol de la variable s.x: Memory Memory Register(FP) El árbol para .A[3]: Memory Operator(-) Memory Operator(*) Operator(-) Memory Constant(2*WORDSIZE) Constant(WORDSIZE) Constant(3) Register(FP)
  167. 167. Estatutos <ul><li>Estatutos de Asignación </li></ul><ul><li><variable> = <expresión> </li></ul><ul><li>es representada por el árbol de ensamblador: </li></ul>Move <variable> <expression>
  168. 168. Ejemplo: Void main() { int x; int y; y = x + 4; } Move Memory Operator(+) Operator(-) Constant(4) Memory Register(FP) Register(FP) Constant(4)
  169. 169. <ul><li>Estatuto If </li></ul><ul><li>if (<test>) <estatuto1> else <estatuto2> </li></ul><ul><li>se puede traducir en </li></ul><ul><li>If (<test>) goto IFTRUE </li></ul><ul><li><código para estatuto2> </li></ul><ul><li>goto IFEND </li></ul><ul><li>IFTRUE: </li></ul><ul><li><código para estatuto1> </li></ul><ul><li>IFEND: </li></ul>
  170. 170. Y se representa con el árbol: Sequential ConditionalJump(“iftrue”) <test> Sequential Sequential Sequential Sequential Label(“ifend”) Jump(“ifend”) Label(“iftrue”) <statement1> <statement2>
  171. 171. <ul><li>Estatuto while: </li></ul><ul><li>while (<test>) <estatuto> </li></ul><ul><li>se puede traducir en </li></ul><ul><li>WHILESTART: </li></ul><ul><li>if (not <test>) goto WHILEEND </li></ul><ul><li><código de estatuto> </li></ul><ul><li>goto WHILESTART </li></ul><ul><li>WHILEEND: </li></ul><ul><li>aunque es mas eficiente </li></ul><ul><li>goto WHILETEST </li></ul><ul><li>WHILESTART: </li></ul><ul><li><código de estatuto> </li></ul><ul><li>WHILETEST: </li></ul><ul><li>if (<test>) goto WHILESTART </li></ul><ul><li>WHILEEND: </li></ul>
  172. 172. <ul><li>Estatuto for: </li></ul>for (<initialize>;<test>;<increment>) <estatement> es equivalent a: <initialize> while (<test>) { <statement> <increment> } Entonces queda como: <initialize> goto FORTEST FORSTART: <statement> FORTEST: <if (<test>) goto FORSTART
  173. 173. <ul><li>Estatuto Return: </li></ul><ul><li>Cuando un estatuto return es ejecutado, el valor que es retornado necesita salvarse en el registro resultado , el registro de activación (RA) actual necesita ser sacado de la pila (POP OFF) y el control necesita ser retornado a la función llamadora. En lugar de duplicar todo el código de ensamblador abstracto para limpiar la pila por cada estatuto return , podemos copiar el valor a ser retornado al registro resultado y entonces saltar al final de la función, donde una sola copia del código que limpia la pila existe. </li></ul>
  174. 174. <ul><li>Definiciones de Función o Métodos: </li></ul><ul><li>El ensamblador abstracto debe: </li></ul><ul><ul><li>Guardar el viejo SP, FP y registros de dirección de retorno en la pila. </li></ul></ul><ul><ul><li>Apuntar el FP al inicio del RA actual. </li></ul></ul><ul><ul><li>Apuntar el SP al final del RA actual. </li></ul></ul><ul><ul><li>Incluir el ensamblador para el cuerpo de la función. </li></ul></ul><ul><ul><li>Restaurar el viejo SP, FP y registros de dirección de retorno (el FP al último si se usa para referenciar los datos en la pila). </li></ul></ul><ul><ul><li>Retornar del método, con una instrucción de ensamblador abstracto return . </li></ul></ul><ul><ul><li>Incluir etiquetas después del cuerpo para instrucciones return desde adentro del cuerpo </li></ul></ul>El código ensamblador queda así: <Etiqueta de inicio de el método> <Código para asignar o meter el registro de activación> <cuerpo de la función> <Etiqueta del fin de método> <Código para sacar el registro de activación> <return>
  175. 175. Creación de Árboles de Ensamblador Abstracto (AAT) en Java <ul><li>Para construir AAT en Java, primero definimos una interfase para construir AATs para estatutos y para expresiones. </li></ul><ul><li>Entonces, se agregan en el análisis semántico llamadas a las funciones definidas en la interfase. </li></ul><ul><li>De esta forma, el analizador semántico construirá los AATs al igual que checará por los errores semánticos. </li></ul>
  176. 176. Etiquetas Se necesitan tanto para las llamadas a métodos como para los “if” y los ciclos “while” y “for”. La siguiente clase nos genera etiquetas únicas (ifEnd001,ifEnd02,etc.) y etiquetas específicas (llamadas a métodos). import java.util.Hashtable; class Label { protected String label; private static HashTable labelhash; Integer last; Int next; public Label(String s) { if (labelhash == null) { labelhash = new HashTable(199); } last = (Integer) labelhash.find(s); If (last == null) { next = 1; labelhash.insert(s, new Integer(1)); label = s + 1; } else { next = last.intValue() + 1; labelhash.delete(s); labelhash.insert(s, new Integer(next)); label = s + next; } } public static Label AbsLabel(String s) { Label abslab = new Label(); abslab.label = s; return abslab; } public String toString() { return label; }
  177. 177. Interfase para Construir Árboles de Ensamblador Abstracto import java.util.Vector; public interface AATBuildTree { public AATSatement functionDefinition(AATStatemen body, int framesize, Label start, Label end); public AATSatement ifStatement(AATExpression test, AATStatement ifbody, AATStatement elsebody); public AATEexpression allocate(AATExpression size); public AATStatement whileStatement(AATExpression test, AATStatement increment, AATStatemen body); public AATStatement emptyStatement( ) ; public AATStatement callStatement(Vector actuals, Label name); public AATStatement assignmentStatement(AATExpression lhs, AATExpressionrhs); public AATStatement sequentialStatement(AATStatement first, AATStatement second); public AATExpression baseVariable(int offset); public AATExpression arrayVariable(AATExpresion base, AATExpression index, int elementSiza); public AATExpression classVariable(AATExpression(AATExpression base, int, offset); public AATExpression constantExpression(int, value); public AATExpression operatorExpression(AATExpression left, AATExpression rigth, int operator); public AATExpression callExpression(Vector actuals, Label name); public AATStatement returnStatement(AATExpression value, Label functioned); }
  178. 178. EJEMPLOS: void foo(int a) { int b; int c; if (a > 2) b = 2; else c = a+ 1; } Sea bt una instancia de una clase que implementa la interfase AATBuildTree, y sea 4 el tamaño de la palabra de la máquina. El árbol de ensamblador abstracto para la expresión (a > 2) en la función foo podría crearse con: AATExpression e1; e1 = bt.operatorExpression(bt.baseVariable(4), bt.constantExpression/(2), AATOperator.GREATER_THAN);
  179. 179. Igual, el ensamblador abstracto para el estatuto b = 2; y c = a + 1 , puede crearse con: AATEstatement s1, s2; s1 = bt.assignmentStatemen(bt.baseVariable(0), bt.constantExpression(2)); s2 = bt.assignment(bt.BaseVariable(-4), bt.operatorExpression(bt.baseVariable(4), bt.constantExpression(1), AATOperator.PLUS)); Finalmente, dadas las definiciones anteriores, el ensamblador abstracto para el estatuto if (a > 2) b = 2 else c = a + 1; puede crearse con: AATstatement s3; s3 = bt.ifStatement(e1,s1,s2);
  180. 180. <ul><li>public AATSatement functionDefinition(AATStatemen body, int framesize, Label start, Label end); El árbol de ensamblador para definición de funciones comienza con la etiqueta para la función, seguido de una serie de instrucciones para salvar los valores viejos de la dirección de retorno, los registros FP y SP, seguido del código para poner nuevos valores del FP y SP, seguido del cuerpo de la función, seguido por una etiqueta para el final de la función, seguido del código para restaurar los valores viejos de la dirección de retorno, registros FP y SP, seguido de un estatuto de ensamblador abstracto return, para regresar el flujo de control a la función llamadora. </li></ul><ul><li>public AATStatement returnStatement(AATExpression value, Label functioned); El árbol de ensamblador para un estatuto return copia el valor del estatuto return dentro de el registro resultado y después salta a la etiqueta del final de la función. </li></ul><ul><li>public AATEexpression allocate(AATExpression size); Esta función es llamada por medio de una expresión new , para asignar espacio en el heap . Es implementada por una llamada a la función intrínseca (build-in) que asigna espacio- igual que una función input/output son implementadas por medio de llamadas a funciones intrínsecas. La función allocate toma un solo parámetro – el tamaño (en bytes) del espacio a asignar – y retorna un apuntador al bloque nuevo asignado. </li></ul>Algunas de las funciones para construír árboles de ensamblador abstracto pueden explicarse mejor:
  181. 181. Modificaciones al Analizador Semántico <ul><li>En lugar de crear un módulo separado que recorra el AST de nuevo para construir un AAT, se modifica el analizador semántico para que mientras checa la semántica vaya construyendo el AAT. </li></ul><ul><li>Cada uno de los métodos para visitar expresiones y estatutos en el analizador semántico retornará dos piezas de información en lugar de una – el tipo del subárbol, como antes, mas un AAT que representa la expresión o el estatuto. </li></ul>
  182. 182. Ejemplo: Para analizar un AST: Integer_Literal(3) El analizador semántico actualmente retorna IntegerType . Para analizar el AST: + Integer_Literal(3) Integer_Literal(4) El analizador semántico llama al método accept para cada subárbol – constant(3) y constant(4), se asegura que ambos sean enteros, y retorne un entero. Después de modificar el analizador semántico para producir AATs, cunado se analiza el AST: Integer_Literal(3) El analizador retornará dos valores: Integer_Type y bt.constantExpression(3).
  183. 183. Cuando se analiza el AST: + Integer_Literal(3) Integer_Literal(4) Primero se llama el método accept de los subárboles, se asegura que los tipos sean enteros y entonces construye una expresión operador basado en los valores de los subárboles (usando una llamada al método operatorExpression en la interfase AATBuildTree ).
  184. 184. VIII Generación de Código <ul><li>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. </li></ul><ul><li>El código debe ser correcto y optimo. </li></ul><ul><li>El generador de código toma las desiciones básicas: </li></ul><ul><ul><li>Asignar Registros.- Registros generales, Prop. Específicos, pares, impares, registros índices, etc. </li></ul></ul><ul><ul><li>Selección de Instrucciones.- La mejor secuencia de instrucciones (la optima). Por ejemplo elegir INC en lugar de MOV – ADD. </li></ul></ul>
  185. 185. <ul><li>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. </li></ul><ul><li>Generaremos código ensamblador por medio de usar “tiles” para rellenar huecos, con el árbol de ensamblador abstracto. </li></ul><ul><li>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. </li></ul><ul><li>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”. </li></ul>
  186. 186. Código Ensamblador Objeto Usaremos un subconjunto del ensamblador para MIPS if rs ≥ 0, jump to the label <target> Bgez rs, <target> if rs < 0, jump to the label <target> bltz rs, <target> if rs ≠ rt, jump to the label <target> bne rs, rt, <target> if rs == rt, jump to the label <target> beq rs, rt, <target> if rs < rt, rd = 1, else rd = 0 slt rd, rs, rt Jump to the address stored in register rs. Used in conjunction with jal to return from function and procedure calls jr rs 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. jal <target> Jump to the assembly label <target>. j <target> Move contents of the special register LOW into the register rd. mflo rd Dive contents of register rs by register rt, put the quotient in register LO and the remainder in register HI div 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 mult rs, rt Add the constant value <val> to register rs, put result in register rt. addi rt, rs, <val> Subtract contents of register rt from rs, put result in register rd. sub rd, rs, rt Add contents of register rs and rt, put result in register rd. add rd, rs, rt 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 sw 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>] Description lw rt, <offset> (base) Instruction
  187. 187. Register that we will use for code generation General Purpose Register $t3 $t3 General Purpose Register $t2 $t2 General Purpose Register $t1 $t1 Accumulator Register – Used for expressioncalculation $t0 $ACC Zero register – This register always has the value 0 $zero $zero Result Register – Holds the return address for the current function $ra $return Result Register – Holds the return value for functions $v0 $result Expression Stack Pointer – The next expression stack holds temporary values for expression avaluations $esp $ESP Stack Pointer – Used for the activation record stack $sp $SP Frame pointer – points to the top of the current activation record Description $fp SPIM Name $FP Mnemonic Name
  188. 188. <ul><li>Usaremos una pila para guardar valores temporales en expresiones. Esta pila estará además de la pila normal de RA. </li></ul><ul><li>Primero describiremos una forma de producir código ineficiente. </li></ul><ul><li>Después veremos como producir código mas eficiente. </li></ul><ul><li>Tiling simple está basado en un recorrido post-order del árbol de ensamblador abstracto. </li></ul><ul><li>Esto es, después de cubrir el árbol con tiles , emitiremos código para cada tile en una forma post-order. </li></ul><ul><li>Primero emitiremos recursivamente tiles para cada uno de los sub-árboles del árbol de ensamblador, de izquierda a derecha. </li></ul><ul><li>Después, emitiremos el código para el tile de la raíz del árbol. </li></ul>Tiling Simple
  189. 189. 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
  190. 190. 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)
  191. 191. ¿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
  192. 192. 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
  193. 193. 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)
  194. 194. 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 in

×