SlideShare una empresa de Scribd logo
1 de 213
CURSO:
PROGRAMACION DE
SISTEMAS
   Dr. Ramón Zatarain Cabada
ELEMENTOS DEL CURSO
 Programa  del Curso..... PDF
 Datos del Profesor …. RZC

 Contenido del Curso…. Diaps

 Proyectos …. Archs

 Software y Herramientas
Unidad I
Introducción a la Programación
de Sistemas
1.1 ¿Qué es y que estudia la P. de S. ?


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

    Ensambladores ….      (notas)

    Compiladores…….       (notas)

    Intérpretes……......   (notas)
1.5 Generador de Código para Compiladores
(Compilador de Compiladores)
   Definición: Un compilador de compiladores o generador de
    Parsers es una utilería para generar el código fuente de un
    parser, intérprete o compilador a partir de una descripción de
    lenguaje anotado en la forma de gramática (usualmente BNF)
    mas código que es asociado con cada una de las reglas de la
    gramática que debe ser ejecutada cuándo esas reglas sean
    aplicadas por el parser. Esas piezas de código son algunas
    veces llamadas rutinas de acciones semánticas ya que ellas
    definen la semántica de las estructura sintáctica y que es
    analizada por el parser. Dependiendo del tipo de parser que será
    generado, las rutinas pueden construir un árbol de parsing (o
    AST) o generar código ejecutable directamente (notas).
Unidad II
Introducción al Diseño de
Lenguajes de Programación
 Visión del Problema.
 Consideraciones Preliminares.

 Objetivos y filosofías del diseño de los

  lenguajes de programación.
 Diseño detallado.

 Caso de estudio.
Unidad III
Análisis de Léxico
3.1 Introducción a los Autómatas Finitos
y Expresiones Regulares


   (ver apuntes de Lenguajes y Autómatas).
3.2 Analizador de Léxico.
   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.
    Ejemplo:
    TIPO         EJEMPLOS
    ID           foo n14 last
    NUM          73 0 00 515
    REAL         66.1 .5 10. 1e67
    IF           if
    COMMA        ,
    NOTEQ !=
    LPAREN       (
Analizador de Léxico (cont.)
 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.
3.3 Manejo de “Buffers”
   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.
    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.
3.4 Creación de la Tabla de
Símbolos.
   Checar notas
    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.
Ejemplo
Ejemplo.-
Programa X1;
       Var X, Y: Integer;
           Z: Real;
           Arreglo: Array [1…100] of int
       Procedure compara (a, b: Integer)
             Var n, m: integer;
             Begin
                      ----
                      ----
             End
Begin
  ----
  ----
End
3.5 Manejo de Errores de Léxico
 Erroresposibles detectados en el análisis de
 léxico son:
    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.
    Caracteres inválidos para el alfabeto del el
     lenguaje.
    Longitud de ciertos tokens demasiado larga.
3.6 Generadores de Código
Léxico
 La construcción de un scanner puede
  automatizarse por medio de un generador de
  analizadores de léxico.
 El primer programa de este tipo que se hizo
  popular fue Lex (Lesk, 1975) que generaba
  un scanner en lenguaje C.
 Hoy en día existen muchos programas de
  este tipo: Flex, Zlex, YooLex, JavaCC,
  SableCC, etc.
Ejemplo: Javacc
   Javacc (Java Compiler Compiler) es un generador
    de scanners y parsers (https://javacc.dev.java.net/).
   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.
   Se puede usar como pre-procesador de Javacc, a
    jjtree y a JTB para la construcción del árbol
    sintáctico.
(cont.)
 USO DE JAVACC


  Miparser.jj       javacc Miparser.jj         Miparser.java
                                               ParseException.java
                                               TokenMgrError.java
                                               otros archivos java


        javac Miparser.java


  Miparser.class    java Miparser <inputfile           mensajes
(cont.)
EJEMPLO DE ESPECIFICACION PARA GENERAR UN SCANNER
PARSER_BEGIN(PS1)
         class PS1 {}
PARSER_END(PS1)

/* Para la expresión regular de la derecha lo de la izquierda será retornado */

TOKEN: {
              <IF: "if">
             |<#DIGIT: ["0"-"9"]>
             |<ID: ["a"-"z"] (["a"-"z"]|<DIGIT>)*>
             |<NUM: (<DIGIT>)+>
             |<REAL: ((<DIGIT>)+ "." (<DIGIT>)*) |
                             ((<DIGIT>)* "." (<DIGIT>)+)>
}

SKIP: {
              <"--" (["a" - "z"])* ("n" | "r" | "rn")>
             |" "
             |"t"
             |"n"
             |"r"
}

void Start():
{}
{             (<IF> | <ID> | <NUM> | <REAL>)* }
Unidad IV
Análisis de Sintáxis
4.1 Introducción a las gramáticas
Libres de Contexto y Árboles de
derivación

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

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

    1) SS;S         2) Sid := E         3) Sprint (L)
    4) E id         5) E num            6) E E + E           7) E(S,E)
    8) L E          9) L L , E

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

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

    Eid  Enum           EE*E        EE/E
    EE+E                 EE-E        E(E)

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

          E
                      E
     E    -   E
                  E   -   E
 E   -    E   3
                  1   E   -   E
 1        2
                      2       3
4.2 Diagramas de Sintaxis
 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.
 Ejemplo en Java (liga)
4.3 Precedencia de Operadores
   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:
(cont.)
SE$                                        nota: “$” es EOF- marker
EE+T            EE-T              ET
T-->T*F          TT/F              TF
Fid             Fnum              F(E)

Esta gramática acepta el mismo lenguaje que la
de sección 4.1 pero solo produce un árbol de
parsing por oración. Esta gramática no podría producir
árboles como los siguientes:
                      X                                            ?U

                      +        ?Y                         ?V           *

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

    Descendentes
    Ascendentes

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

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

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

Nuestro Parser tendría 3 métodos (uno por cada producción)
Programa del Parser
Final int if = 1, then = 2, else = 3, begin = 4, end = 5, print = 6, semi = 7,
                      num = 8, EQ = 9

int tok = get token ( );

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

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

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

 Suponer  que queremos construír un Parser
 predictivo para la gramática de sección 4.3.
 SE$           EE+T            EE-T             ET
 T-->T*F        TT/F            TF
 Fid           Fnum            F(E)




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

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

    En dicho caso podemos factorizar por la izquierda la gramática. El
    resultado sería el sig:
         IF-STM  if EXP then STM X
         X λ
         X  else IF-STM

    las producciones anteriores facilitarán el trabajo del parser
    predictivo.
4.4.2 Analizador ascendente(LR y
     LALR).
   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.
   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).
   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.
(cont.)
 Un    Parser LR(K) está compuesto de:
      La cadena de entrada
      Una pila
      Una tabla de Parsing LR
 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
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
Ejemplo: Parser LR(K)
                                   TABLA DE PARSING
     id         num    print   ;       ,    +     :=   (    )      $     S      E    L
1         S4            s7                                               g2
2                              s3                                  a
3         S4            s7                                               g5
4                                                 s6
5                              r1      r1                          r1
6         S20    s10                                   s8                      g11
7                                                      s9
8         S4            s7                                               g12
9     S20       s10                                                            g15   g14
10                             r5     r5    r5               r5     r5
11                             r2     r2    s16                     r2
12                             s3     s18
13                             r3     r3                           r3
14                                    s19                    s13
15                                    r8                    r8
16    S20       s10                                    s8                      g17
17                             r6     r6    s16             r6     r6
18    S20       s10                                    s8                      g21
19    S20       s10                                    s8                      g23
20                             r4     r4    r4              r4     r4
21                                                          s22
22                             r7     r7    r7              r7     r7
23                                    r9    s16             r9
Ejemplo (cont.):
         PILA               ENTRADA                   ACCION
  1                  a:=7;B:=c+(d:=5+6,d)$           shift
  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
1 id4 := 6 E11        ;B:=c+(d:=5+6,d)$           reduce
                                                  S->id:=E
                                          .            .
   .
                                          .            .
   .
                                          .            .
   .
                                          .            .
   .
                                          .            .
   .
                                          .            .
   .
                                          .            .
   .
  1 S2                                        $      accept
Parsing LALR
   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.
   Esta técnica fue inventada por DeRemer en 1971.
   Casi cualquier construcción sináctica de un LP puede
    ser expresado de manera conveniente por una
    gramática LALR.
   Generadores de Parsers famosos como YACC (Yet
    Another Compiler-Compiler-Johnson) producen un
    parser LALR.
   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.
4.5 Administración de tablas de
               símbolos.

 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.
 Esto ocurre al detectarse las declaraciones
  en el programa fuente.
 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).
4.6 Manejo de errores sintácticos y su
               recuperación.

   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.
   ¿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.
   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.
(cont.)
   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.
   Esto se puede realizar por medio de borrar,
    reemplazar o insertar tokens.
   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.
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.
4.7 Generadores de código para
         analizadores sintácticos
   Como se vio al final del capítulo 3, Javacc es un generador
    de scanners y de parsers (https://javacc.dev.java.net/).
   Javacc produce un parser del tipo descendente (recursivo) o
    LL(k) donde k (lookahead) puede ser cualquier número entero
    positivo.
   El parser producido puede correr en cualquier plataforma que
    cuente con el compilador de Java.
   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.
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 : {
                      ""
         |            "t"
         |            "n"
         |            "r"
         }
(cont. Ejemplo)


TOKEN : {                                                                   void Program() :
                                                                            {}
            <INT: "INT">                                                    { S() <EOF> }
|           <IF: "if">
|           <THEN: "then">                                                  void S() :
|           <ELSE: "else">                                                  {}
|           <BEGIN: "begin">                                                {
|           <PRINT: "print">                                                  <BEGIN> S() L()
|           <END: "end">                                                    | <PRINT> E()
                                                                            | LOOKAHEAD(12) "if" E() "then" S() "else" S()
|           <SEMI: ";">                                                     | "if" E() "then" S()
|           <EQUAL: "=">                                                    }
|           <ID: (["a"-"z"]|["A"-"Z"]) (["a"-"z"]|["A"-"Z"]|["0"-"9"])* >   void L() :
}                                                                           {}
                                                                            { <END>
                                                                            | <SEMI> S() L()
                                                                            }

                                                                            void E() :
                                                                            {}
                                                                            { <ID> <EQUAL> <ID> }
Unidad V
Análisis Semántico
5.1 Analizador semántico

   Un compilador no solo tiene que revisar la sintaxis de
    código fuente, si no también la semántica de este.
   Al igual que en los lenguajes naturales (español, ingles,
    etc.) en los lenguajes de programación existen reglas
    semánticas para definir el significado de los programas,
    estatutos, expresiones, etc.
   Por ejemplo un error semántico es usar (en pascal ó
    java) un identificador que no fue anteriormente
    declarado.
   Otro ejemplo de error semántico en un programa es
    cuando este es compilado y y no se detectan errores
    pero el momento de ser ejecutado este programa no
    funciona correctamente.
5.2 Verificación de tipos en
            expresiones.
 Cuando    mezclamos diferentes tipos en una
  misma expresión o que llamamos una rutina
  que no existe existe un error semántico.
 Una de las funciones del analizador smántico
  es verificar que los tipos de una expresión
  sean compatibles entre si.
 Para hacer lo anterior el compilador cuenta
  con información de los atributos (tipos,
  tamaño, número de argumento, etc.) de los
  identificadores en una tabla de símbolos.
5.3 Conversión de tipos.

   Algunas veces los tipos de una expresión o estatuto son
    diferente.
   Por ejemplo en la asignación,
                  a = b * c;
    el tipo del resultado de evaluar b*c es diferente al de el
    identificador a.
   El compilador algunas veces con ciertos diferentes tipos puede
    hacer una conversión interna en forma implícita para solucionar
    el problema. Otras veces el programador explícitamente es el
    que hace la conversión (casting).
    Ejemplo:
          float   dinero;
          int     cambio;
          dinero = (float) cambio;
5.4 Acciones agregadas en un Analizador
    sintáctico descendente (top-down).
   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.
   Cada símbolo terminal y noterminal puede asociarse con su propio tipo de
    valor semántico.
    Por ejemplo en la siguiente gramática para YACC de una calculadora
    simple, el tipo asociado con exp e INT podría ser int:

         %token INT PLUS MINUS TIMES UMINUS
         %start exp
         %left PLUS MINUS
         %left TIMES
         %left UMINIS
         exp: INT | exp PLUS exp | exp MINUS exp | exp TIMES exp 1
                          MINUS exp       %prec UMINUS
(cont.)
 Los  otros tokens no necesitarían tener un
  valor.
 Por otra parte el tipo asociado a un token
  debe por supuesto coincidir con el tipo de
  token que el scanner retorne.
 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.
Recursivo-descendente
   En un parser recursivo-descendente, las acciones
    semánticas son los valores retornados por las
    funciones de parsing, o los efectos laterales de
    esas funciones o ambos.
   Por cada símbolo terminal y noterminal, asociamos
    un tipo (desde el lenguaje de implementación del LP
    del compilador) de valor semántico representando
    frases derivadas desde ese símbolo.
   El siguiente programa es un intérprete recursivo
    descendente para una parte de la gramática en la
    cual eliminamos la recursión por la izquierda (por
    conveniencia la volvemos a mostrar):
(cont.)

S  E$             E  T E’           E’  + T E’         E’  - T E’        E’  λ
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.
Interprete                                               Acciones semánticas


class Token2 {
               int kind; Object val;
               Token2(int k, Object v)                           int T() {switch(tok.kind) {
               {                                                    case ID:
                 kind=k;                                            case NUM:
                 val=v;                                             case LPAREN: return Tprime(F());
               }                                                    default: print("2 esperaba ID, NUM o parent izq");
}                                                                               //skipto(T_follow);
final int EOF=0, ID=1, NUM=2, PLUS=3,                                           return 0;
               MINUS=4,LPAREN=5, RPAREN=6, TIMES=7;              }}
int lookup(String id) { …. }
int F_follow[] = {PLUS,TIMES,RPAREN,EOF};                        int Tprime(int a) {switch (tok.kind) {
                                                                    case TIMES: eat(TIMES); return Tprime(a*F());
int F() {switch(tok.kind) {                                         case PLUS:
   case ID: int i=lookup((String)(tok.val));advance()return i;      case RPAREN:
   case NUM: int i=((integer)(tok.val)).intVal();                   case EOF: return a;
               advance();return i;                                  default: print("3 esperaba ID, NUM o parent izq");
   case LPAREN: eat(LPAREN);                                                    //skipto(T_follow);
               int i=E();                                                       return 0;
               eatOrSkipTo(RPAREN,F_follow);                     }}
               return i;
   case EOF:                                                      void eatOrSkipTo(int expected, int[] stop) {
               default: print("1 esperaba ID,NUM, o parent izq");              if (tok.kind==expected)
               //skipto(F_follow); return 0;                                                  eat(expected);
}}                                                                             else {print("4 esperaba ID, NUM o parent izq");
                                                                                            //skipto(stop);}
int T_follow[]= {PLUS,RPAREN,EOF};                               }
Parser Automáticamente generado

   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.
    Ejemplo:

     void Start():                              Int Term():
     { int i; }                                 { int a,i; }
     { i=Exp() <EOF> {System.Out.println(i);}   { a=factor()
     }                                                         ( “*” i=Factor() { a=a*i;}
     Int Exp():                                                | “/” i=Factor() {a=a/i;}
     { int a,i; }                                              )*
     { a=Term()                                                { return a; }
                  ( “+” i=Term() {a=a+i;}       }
                  | “-” i=Term() {a=a-i;}       Int Factor():
     )*                                         { Token t; int i; }
     { return a; }                              { t=<IDENTIFIER> { return lookup(t.image); }
     }                                          | t=<INTEGER_LITERAL>
                                                             {return Integer.parseInt(t.image); }
                                                | “(“ i=Exp() “)” {return i; }
                                                }
Árboles de Parsing Abstractos
   Para mejorar la modularidad del compilador, es recomendable separar detalles de la
    sintaxis con detalles de la semántica (chequeo de tipos y traducción a código
    máquina).
   Una forma de hacerlo es producir un árbol de sintaxis abstracta (una forma
    condensada de árbol de parsing útil para representar construcciones del LP).
   Por ejemplo la producción S  if B then S1 else S2 pudiera aparecer en un arbol
    sintáctico como:

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

                                                          If   Exp ( Exp ) Est

                       B           S1          S2

    En un árbol sintáctico, los operadores y las palabras claves (reservadas)
    no aparecen como hojas, sino que están asociadas con el nodo interior
    que sería el padre de esas hojas en el arbol de parsing.
(cont.)
 Otro    ejemplo es en el árbol de parsing:
                       L

             E        +        T

    T        *        F        F

    F                 4        8

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

                                          2       4
Ejemplo:
   La gramática siguiente nos muestra una sintaxis
    abstracta de un lenguaje para expresiones:
        EE+E       EE-E       EE*E
        EE/E       Eid Enum
   Esta gramática es impráctica para un parser ya que
    es ambigua pues no tiene precedencia de
    operadores.
   Sin embargo, esta gramática no es para el parser.
    El analizador semántico podría usarla el cual no se
    molesta por la ambiguedad puesto que ya tiene su
    arbol.
Árboles de Sintaxis en Java
 EnJava 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.
Programa de clases para Exp
public abstract class ExpCh4 {
              public abstract int eval();
}
class PlusExp extends ExpCh4 {
              private ExpCh4 e1,e2;                        class DivideExp extends ExpCh4 {
              public PlusExp(ExpCh4 a1, ExpCh4 a2)                        private ExpCh4 e1,e2;
                              {e1=a1; e2=a2;}                             public DivideExp(ExpCh4 a1, ExpCh4 a2)
              public int eval() {                                                         {e1=a1; e2=a2;}
                             return e1.eval()+e2.eval();                  public int eval() {
              }                                                                          return e1.eval()/e2.eval();
}                                                                         }
class MinusExp extends ExpCh4 {                            }
              private ExpCh4 e1,e2;                        class Identifier extends ExpCh4 {
              public MinusExp(ExpCh4 a1, ExpCh4 a2)                       private String f0;
                              {e1=a1; e2=a2;}                             public Identifier(String n0) {f0=n0;}
              public int eval() {                                         public int eval() {
                             return e1.eval()-e2.eval();                                 return (7); //return lookup(f0);
              }                                                           }
}                                                          }
class TimesExp extends ExpCh4 {                            class IntegerLiteral extends ExpCh4 {
              private ExpCh4 e1,e2;                                       private String f0;
              public TimesExp(ExpCh4 a1, ExpCh4 a2)                       public IntegerLiteral(String n0) {f0=n0;}
                              {e1=a1; e2=a2;}                             public int eval() {
              public int eval() {                                                        return (4);
                             return e1.eval()*e2.eval();                                  //return Integer.parseInt(f0);
              }                                                           }
}                                                          }
(cont.)
   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.
     S  E$
     ETE’        E’  + T E’   E’  - T E’   E’  λ
     T  F T’     T’  * F T’   T’  / F T’   T’  λ
     F  id       F  num       F  (E)

    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.
Gramática con acciones semánticas para
                árboles sintácticos


PARSER_BEGIN(InterSinTree)
            class InterSinTree {}
PARSER_END(InterSinTree)

TOKEN: {                                                                     ExpCh4 Exp():
                                                                                              { ExpCh4 e1,e2; }
                  <#DIGIT: ["0"-"9"]>                                                         { e1=Term()
                  |<ID: ["a"-"z"] (["a"-"z"]|<DIGIT>)*>                                            ("+" e2=Term() { e1=new PlusExp(e1,e2);}
                  |<INTEGER_LITERAL: (<DIGIT>)+>                                                   |"-" e2=Term() { e1=new MinusExp(e1,e2);}
                                                                                                   )*
}                                                                                                  { return e1;}
                                                                                              }
SKIP: {                                                                      ExpCh4 Term():
                   <"--" (["a" - "z"])* ("n" | "r" | "rn")>                               { ExpCh4 e1,e2; }
                  |" "                                                                        { e1=Factor()
                  |"t"                                                                            ("*" e2=Factor() { e1=new TimesExp(e1,e2);}
                  |"n"                                                                            |"/" e2=Factor() { e1=new DivideExp(e1,e2);}
                  |"r"                                                                            )*
}                                                                                                  { return e1;}
                                                                                              }

ExpCh4 Start():                                                              ExpCh4 Factor() :
                  { ExpCh4 e; }                                                             { Token t; ExpCh4 e; }
                  { e=Exp() <EOF>                                                           { (t=<ID>
                                {System.out.println(e.eval()); return e; }                                     {return new Identifier(t.image); } |
                  }                                                                            t=<INTEGER_LITERAL>
                                                                                                               {return new IntegerLiteral(t.image); } |
                                                                                               "(" e=Exp() ")" {return e; })
                                                                                            }
VISITADORES
   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.
(cont.)
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.
Sintáxis Abstracta para MiniJava
   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:
    public class ExpList {
            private Vector list;
            public ExpList() {
                          list=new vector();
            }
            Public void addElement (Exp n) {
                          list.addElement(n);
            }
            public Exp elementAt(int i) {
                          return (exp)list.elementAt(i);
            }
            public int size() {
                          return list.size();
            }
    }
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)
(cont. figura)
Abstract class Exp
And(Exp e1, Exp e2)
LessThan(Exp e1, Exp e2)
Plus(Exp e1, Exp e2)             Minus(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()
Arbol Sintáctico
 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.
public interface Visitor {
                     public void visit(Program n);
                     public void visit(MainClass n);
                     public void visit(ClassDeclSimple n);
                     public void visit(ClassDeclextends n);
                     public void visit(VarDecl n);
                     public void visit(MethodDecl n);
                     public void visit(Formal n);
                     public void visit(IntArrayType n);
                     public void visit(BooleanType n);
                     public void visit(IntegerType n);
                     public void visit(IdentifierType n);
                     public void visit(Block n);
Visitador            public void visit(If n);
                     public void visit(While n);
MiniJava             public void visit(Print n);
                     public void visit(Assign n);
                     public void visit(ArrayAssign n);
                     public void visit(And n);
                     public void visit(LessThan n);
                     public void visit(Pluss n);
                     public void visit(Minus n);
                     public void visit(Times n);
                     public void visit(ArrayLoockup n);
                     public void visit(ArrayLength n);
                     public void visit(Call n);
                     public void visit(IntegerLiteral n);
                     public void visit(True n);
                     public void visit(False n);
                     public void visit(IdentifierExp n);
                     public void visit(This n);
                     public void visit(NewArray n);
                     public void visit(NewObject n);
                     public void visit(Not n);
                     public void visit(Identifier n);
            }
(cont. Arbol Sintáctico)
   Podemos construir un árbol sintáctico usando expresiones new
    anidadas. Por ejemplo el árbol sintáctico para el estatuto MiniJava:
         x = y.m(1,4+5);

    usaría el siguiente código:

         ExpList el= new ExpList();
         el.addElement(new IntegerLiteral(1));
         el.addelement(new Plus(new IntegerLiteral(4),
                                 new IntegerLiteral(5)));
         Statement s = new Assign(new Identifier “x”),
                          new Call(new identifierExp(“y”),
                                    new Identifier(“m”),
                                         el));
5.5 Pila semántica en un analizador
sintáctico ascendente (bottom-up).

   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.
   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.
   Estos atributos estarían ligados a la correspondiente producción
    en la tabla de parsing
    (consultar sección 5.3 del libro de “Aho, Ullman, Sethi” para ver
    mas detalles de la implementación).
5.6 Administración de la tabla de
símbolos

   El análisis semántico conecta las definiciones de las
    variables con sus usos, checa que cada expresión
    tenga un tipo correcto y traduce la sintaxis abstracta
    a una representación mas simple para generar
    código máquina.
   Esta fase es caracterizada por el mantener la tabla
    de símbolos (también llamada “environment”) la
    cual mapea identificadores con sus tipos y
    localidades.
   Cada variable local en un programa tiene un ámbito
    (scope) dentro del cual es visible. Por ejemplo, en
    un método MiniJava m, todos los parámetros
    formales y variables locales declarados en m son
    visibles solo hasta que finalice m.
(cont.)
 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.
Ejemplo:
       1    Class C {
       2      int a, int b; int c;
       3      public void m() {
       4        System.out.println(a+c);
       5        int j=a+b;
       6        String a=“hello”;
       7        System.out.println(a);
       8        System.out.println(j);
       9        System.out.println(b);
       10     }
       11   }

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.
Implementación de la Tabla
 Existendos 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.
Múltiple Tablas de Símbolos
 Enalgunos 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).
Structure M = struct                 Package M;
          structure E = struct       class E {
                     val a= 5;                 static int a=5;
          end                        }
          structure N = struct       Class N {
                     val b=10                  static int b=10;
                     val a=E.a+b               static int a=E.a+b;
          end                        }
          structure D = struct       Class D {
                     val d=E.a+N.a             static int d=E.a+N.a;
          end                        }
end
(cont.)
   Al anlizar los 2 programas anteriores, sea z0 el
    ambiente base conteniendo funciones predefinidas,
    y sea
    z1={aint}
    z2={Ez1}
    z3={bint,aint}
    z4={Nz3}
    z5={dint}
    z6={Dz5}
    z7=z2+z4+z6
(cont.)
 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}.
 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.
TABLAS DE SIMBOLOS EN
LENGUAJES IMPERATIVOS
   Un programa grande puede contener miles de
    distintos identificadores. Esto hace que la búsqueda
    en la tabla (loock up) tenga que ser eficiente.
   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”.
(cont.)
 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.
SIMBOLOS
 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.
 El módulo símbolo implementa los símbolos
  y tiene estas propiedades:
    Comparar símbolos por igualdad o por mayor es
     rápido (comparación por apuntador o por entero).
    Extraer una llave “hash” de tipo entero es rápido
(cont.)
   Los ambientes son implementados en la clase Symbol.Table
    como Tables mapeando Symbols a ligados (bindings).
   Para eso se manejan ligaduras para diferentes propósitos en el
    compilador – ligadura para tipos, para variables, para funciones,
    etc.
   Entonces, una ligadura es un Objeto.
   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.
   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.
(cont.)
   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.
   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).
   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.
Chequeo de Tipos en MiniJava
   ¿Con que se llena una tabla de símbolos? Esto es,
    ¿Qué es la ligadura o “binding”?
   Para realizar el chequeo de tipos de programas
    MiniJava, la tabla de símbolos debe contener toda
    la información declarada:
       Cada nombre de variable y nombre de parámetro formal
        debe ser ligado a su tipo.
       Cada nombre de método debe ser ligado a sus
        parámetros, tipo de resultado y variables locales.
       Cada nombre de clase debe ser ligado a su variable y
        declaraciones de métodos.
       Liga a página con mas información.
(cont.)
 Por   ejemplo, considere la siguiente figura,
    que muestra un programa y su tabla de
    símbolos.                             PARAMS
                                                            p      int
Class B {
                                                            q      int
  C f; int [ ] j; int q;
  public int start(int p, int q) {       FIELDS
    int ret; int a;                      f        C         LOCALS
    /* …… */                             j        int [ ]   ret    int
    return ret;                          q        int       a      int
                                     B
  }                                  C   METHODS
  public boolean stop(int p) {           start   int
              /* ….. */                  stop    bool       PARAMS
    return false;
                                                            p      int
  }
}                                        ……                 LOCALS
Class C {
    /* ….*/
}
(cont.)
   Los tipos primitivos en MiniJava son int y boolean;
    todos los otros tipos son arreglo de enteros o
    nombres de clases.
   Por simplicidad todos los tipos son “string”, en lugar
    de símbolos; esto nos permite checar igualdad de
    tipos por medio de comparación de “strings”.
   El chequeo de tipos de un programa MiniJava
    ocurre en dos fases. Primero, construimos la tabla
    de símbolos y después checamos los tipos de los
    estatutos y las expresiones. Lo hacemos en dos
    fases porque en MiniJava (igual que en Java) las
    clases son mutuamente recursivas.
(cont.)
   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.
   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.
   El método visitador checa si la variable ya fue
    declarada anteriormente.
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( ));
(cont.)
   La segunda fase del checador de tipos puede ser implementada
    por un visitador que cheque tipos de todas los estatutos y
    expresiones.
   El tipo del resultado de cada método visitador es String, que
    representa los tipos de MiniJava.
   La idea es que cuando el visitador visita una expresión, entonces
    retorna el tipo de esa expresión.
   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).
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( );
}
5.7 Manejo de errores semánticos.

   Cuando el checador de tipos detecta un error de
    tipos o un identificador no declarado, debe imprimir
    el mensaje de error y continuar.
   Esto debido a que normalmente el programador
    prefiere que le describan todos los errores posibles
    del programa fuente.
   Esto quiere decir, que si un error de tipos es
    encontrado, no debe producirse un programa objeto
    por parte del compilador.
   Así, las siguientes fases no deben ejecutarse.
   Hasta esta etapa (chequeo de tipos), la parte del
    compilador se conoce con el nombre de “front End”.
REGISTROS DE ACTIVACION
 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).
 Diferentes invocaciones a la función pueden
  existir a la vez, y cada invocación tiene su
  propia “instanciación” de variables.
(cont.)
 En   el siguiente método de Java
  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.
(cont.)
 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.
MARCOS DE PILA
   Debido a que se trabaja con bloques de datos por
    función un “push” y “pop” no funciona.
   Entonces la pila es tratada como si fuera un gran
    arreglo, con un registro especial- el “stack pointer”
    que apunta a una localidad.
   Todas las localidades después del apuntador son
    basura y todas las que están antes están
    asignadas.
   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.
(cont.)
 El diseño de la estructura de los marcos es
  de acuerdo con la arquitectura y el LP que se
  compila.
 Aunque normalmente el constructor de la
  arquitectura define un diseño de marco
  standard para todos los compiladores para
  esa arquitectura.
Ejemplo: Un marco de pila
                                   Direcciones de Memoria
                                   mas altas

Argumentos    Argumento n           Marco anterior
de entrada    …
              …
              Argumento 1
Apuntador     Liga estática
Del marco

                 Variables
                 locales



                                    Marco
              Dirección retorno
                                    actual
              Temporales

              Registros salvados


               Argumento m
Argumentos     …
De salida      …
               Argumento 1
  Apuntador    Liga estática
  De pila

                                      Marco siguiente
Marco de Pila

   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).
   Cuando la función actual llama otras funciones,
    puede usar el espacio de los argumentos de salida
    para pasar parámetros.
   La dirección de retorno es creada por la instrucción
    CALL.
   Las variables locales también tienen su espacio.
   Las variables mantenidas en registros algunas
    veces son salvadas a memoria.
El Apuntador de Marco (FP)
   Suponer que una función g(…) llama la función f(a1,…an).
    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.
   El viejo SP se convierte en el actual FP y el viejo FP es salvado
    en el marco.
   Cuando FP termina, solo copia FP de regreso a SP y regresa el
    valor viejo salvado de FP.
   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.
Registros
   Por eficiencia, es importante mantener las variables
    locales, resultados intermedios y otros valores en
    registros en lugar de la pila de marcos.
   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.
   ¿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.
Pase de Parámetros
 Estudios actuales han mostrado que
  raramente una función pasa mas de 4
  parámetros.
 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.
Direcciones de Retorno
 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.
 En máquinas modernas la dirección de
  retorno es pasada a un registro en lugar de la
  memoria.
 En funciones “hoja” la dirección no necesita
  ponerse en la pila.
Registros vs. Memoria
   Registros siempre deben usarse en asignación a
    menos que:
       La variable sea pasada por referencia
       La variable es accesada por una función anidada dentro de
        la función actual.
       El valor es demasiado grande para un registro.
       La variable es un arreglo, donde es necesario realizar
        aritmética de direcciones.
       El registro que contiene la variable es necesitado para
        otros propósitos.
       Existen demasiadas variables locales y valores temporales
Ligas Estáticas (Static Links)
   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).
   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).
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
Ligas Estáticas (cont.)
   Existen varios métodos para solucionar lo anterior:
       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.
       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”.
   A continuación describimos el método de liga
    estática para el ejemplo de la diapositiva anterior.
(cont.)
   Línea 21: prettyprint llama show, pasando el apuntador del marco del propio
    prettyprint como una liga estática de show.
   Línea 10: show guarda su liga estática (la dirección del marco de prettyprint)
    dentro de su propio marco.
   Línea 15: show llama indent, pasando su propio apuntador de marco como liga
    estática de indent.
   Línea 17: show llama a show,pasando su propia liga estática (no su propio
    apuntador de marco) como la liga estática.
   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).
   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.
   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.
Unidad VI
Generación de Código
Intermedio
6.1 Lenguajes intermedios.

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

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

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

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

        Las operaciones serían:
        push 1 (meter 1)
        push 2
        ' + ' requiere de 2 operandos, se sacan, se suman y se mete el resultado (3)
        push 1
        push 2
        ' * ' se mete el resultado(2)
        ' - ' se mete el resultado (1)

   Notación prefija: El orden es operador, primer operando, segundo operando.
6.3 Representación de código
intermedio.

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

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

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

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

   En el código de 2 direcciones se evita el uso se
    variables temporales. El formato es:
    Operador argumento1 argumento2
    EJEMPLO: Z = X + Y - X * Y
    1.       ADD        X      Y
    2.       MUL        X      Y
    3.       SUB        (1)   (2)
    4.       STORE      (3)   (Z)

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

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

   Su ventaja es su portabilidad: el mismo código binario puede ser
    ejecutado en diferentes plataformas y arquitecturas. Es la misma
    ventaja que presentan los lenguajes interpretados.
   Sin embargo, como el bytecode es en general menos abstracto,
    más compacto y más orientado a la máquina que un programa
    pensado para su modificación por humanos, su rendimiento
    suele ser mejor que el de los lenguajes interpretados.
   A causa de esa mejora en el rendimiento, muchos lenguajes
    interpretados, de hecho, se compilan para convertirlos en
    bytecode y después son ejecutados por un intérprete de
    bytecode.
   Entre esos lenguajes se encuentran Perl, PHP y Python. El
    código Java se suele trasmitir como bytecode a la máquina
    receptora, que utiliza un compilador just-in-time para traducir el
    bytecode en código máquina antes de su ejecución.
La Máquina Virtual de Java
          (JVM)
 La siguiente liga nos lleva a la información de
 lo que es la JVM y mas sobre Java
 bytecodes.
Árboles de Ensamblador

 Un paso antes de generar Ensamblador Real
 Permite mas fácil optimización de código

 Primero vamos a estudiar la implementación
  de asignación de memoria en el código
  generado (stack y heap).
Implementación de variables
 En Java las variables locales son
  almacenadas en la pila y en registros.
 Las variables de arreglos y de clases son
  almacenadas en el heap.
 Registros de Activación (RA).
     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.
Por ejemplo, la función:

 Int foo() {
     int a;
     int b;
     /* body of foo */
 }



Tendría el registro de activación


          a                         Existen dos apuntadores. FP que
                         FP
                                    apunta al inicio del RA actual y SP que
          b                         apunta a la primera localidad vacía. El
 Registros                          valor retornado de una función es
 salvados                           puesto en un registro especial en lugar
                                    de la pila. Cuando una función es
                         SP         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.
(cont.)
 Como   el FP siempre apunta al comienzo del
  RA actual, podemos acceder a las variables
  locales usando un offset desde el FP.
 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.
Formato completo de RA

                              Input Parameter n

Previous Activation Record            …
                              Input Parameter 2
                              Input Parameter 1
                                                  FP
                             Local Variables


Current Activation Record

                             Saved Registers


                                                  SP
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 */
}
En label A de el programa, la pila de RA se miraría de la
forma siguiente


                   u                 FP
                   v                              Activation
                                                  Record for
             Saved Registers                      main

                                     SP




 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.
En etiqueta B los RA se
mirarían así:                                         u
                          Activation Record           v
                                    for main    Saved Registers

                                                      b
                                                      a
                                                      w
                                                       x
                          Activation Record
                                     for bar
                                                Saved Registers

                                                      d
                                                      c
                                                       y          FP
                          Activation Record
                                                      z
                                      for foo
                                                Saved Registers

                                                                  SP
(cont.)
   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).
   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.
Ensamblador Abstracto

   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.
   Existen cinco diferentes tipos de árboles de
    expresiones:
       Constante. Consiste de un valor de la constante (solo
        enteros).
       Registro. Una cadena que especifica el registro.
       Operador. Dos árboles de expresión para los operandos, y
        el operador: +,-,*,/,<,>,<=,>=,=,&&,||,!.
(cont.)
   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:

                             CallExpresion(“foo”)




               Constant(3)   Constant(4)     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.
   Memoria. Solo se denota la dirección de memoria.
    Ejemplo:
                                        Memory




                                    Constant(1006)



    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




                                       Operator(-)




                     Register(FP)                 Constant(4)
   Existen 8 tipos de árboles de estatutos:

       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.
       Label (Etiqueta). Se usan como destinos de “jumps” y “calls”.
       Jump. Saltos incondicionales contienen una etiqueta.
       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.
       Sequential. Tiene dos subárboles. Representa la ejecución del izquierdo
        seguido del derecho.
       CallStatement. Contienen una árbol de etiqueta, y un árbol expresión de
        cada uno de los parámetros actuales. “Calls” representan “void function
        calls”.
       Empty. Son “no-ops” y son removidos cuando se traduce a ensamblador
        real.
       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”.
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;
  }
X=1;


                Move



         Memory Constant(1)



       Register(FP)
y = a * b;
                             Move




           Memory                                  Operator(*)

                                      Memory                       Memory
        Operator (-)


                                    Operator (+)                 Operator (+)

Register(FP) Constant(4)


                       Register(FP)     Constant(4)     Register(FP)   Constant(8)
Y++;

                             Move




           Memory                                  Operator(+)

                                      Memory                     Constant(1)
        Operator (-)


                                    Operator (-)

Register(FP) Constant(4)


                       Register(FP)     Constant(4)
bar(y,x+1,a);


                            CallStatement(“bar”)


                                  Operator (+)              Memory
         Memory


       Operator (-)        Memory   Constant(1)           Operator (+)




Register(FP) Constant(4)    Register(FP)           Register(FP) Constant(4)
x=function(y+1,3);

                Move


  Memory                      Callexpression(“function”)


Register(FP)             Operator(+)            Constant(3)


                          Memory       Constant(1)


                         Operator(-)


               Register(FP)       Constant(4)
if (x > 2) z = true; else z = false;

                        Sequential
                                     Sequential
ConditionalJump(“iftrue”)
                                              Sequential
    Operator(>)                                            Sequential
                                         Jump(“ifend”)
                                 Move                                Sequential
 Memory Constant(2)
                                                   Label(“iftrue”)
                                                                             Label(“ifend”)
                        Memory       Constant(0)                     Move
Register(FP)
                       Operator(-)                           Memory         Constant(1)

                                                           Operator(-)
                  Register(FP) Constant(8)

                                                     Register(FP) Constant(8)
Creación de Ensamblador Abstracto

 Variables
    Variables base
                   void foo() {
                       int x;
                       int y;
                       /* Body of foo */
                   }


    La variable x y y se representarían con los
     árboles:                            Memory
                         Memory

                                                    Operator(-)
                   Register(FP)

                                           Register(FP)    Constant(4)
   Variables Arreglo: Son almacenadas en el
    heap, y usando un apuntador a la base del
    arreglo el cual se almacena en la pila.
    La siguiente función o método

     void foo arrayallocation() {
         int x;
         int A[];
         int B[];
         A = new int [5];
         B = new int [5];
         /* Body of function */
     }
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[0]
FP
                                                  A[1]
         A
         B                                        A[2]
                                                  A[3]
                                                  A[4]
             Saved Registers
                                                  B[0]
                                                  B[1]
                                                  B[2]
  SP
                                                  B[3]
                                                  B[4]
¿Cómo debemos representar
el arreglo A[3]
                               Memory


                              Operator(-)


                 Memory                     Operator(*)

               Operator(-)   Constant(WORDSIZE)      Constant(3)



Register(FP)       Constant(WORDSIZE)
¿ Y A[x] ?

                                Memory


                               Operator(-)


                  Memory                     Operator(*)

                Operator(-)   Constant(WORDSIZE)           Memory


                    Constant(WORDSIZE)               Register(FP)
 Register(FP)
¿ Y para A[B[2]] ?
                                 Memory


                                Operator(-)


                 Memory                         Operator(*)

               Operator(-)    Constant(WORDSIZE)              Memory

                                                           Operator(-)
Register(FP)       Constant(WORDSIZE)
                                                                 Operator(*)
                                         Memory
                                                     Constant(WORDSIZE)
                                       Operator(-)                     Constant(2)



                        Register(FP)    Constant(2*WORDSIZE)
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 */
     }
El RA y el Heap se ven así:

            Stack             Heap
FP      i                             C[0]
        C                             C[1]
                                      C[2]
            Saved Registers          C[0] [0]
                                     C[0] [1]
SP
                                     C[1] [0]
                                     C[1] [1]
                                     C[2] [0]
                                     C[2] [1]
La variable C sería representada así:



                           Memory

                         Operator(-)



          Register(FP)       Constant(WORDSIZE)
La variable C[2] así:


                                 Memory


                                Operator(-)


                   Memory                     Operator(*)

                 Operator(-)   Constant(WORDSIZE)      Constant(2)



  Register(FP)       Constant(WORDSIZE)
La variable c[2] [1]

                                  Memory


                              Operator(-)


                  Memory                    Operator(*)

               Operator(-)   Constant(WORDSIZE)      Constant(1)


      Memory                  Operator(*)
    Operator(-)
                   Constant(WORDSIZE) Constant(2)
Register(FP) Constant(WORDSIZE)
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 */
             }
¿Cuál es el árbol de ensamblador
para s.y ?
 Él árbol de s:         Memory



                        Register(FP)




 Para obtener y de s:

                             Memory

                           Operator(-)

                    Memory       Constant(WORDSIZE)

                  Register(FP)
Memory
El árbol de la variable s.x:
                                              Memory


                                           Register(FP)


  El árbol para .A[3]:     Memory

                          Operator(-)

                Memory                  Operator(*)

            Operator(-)   Constant(WORDS Constant(3)
                                IZE)

   Memory         Constant(2*WORD
                        SIZE)
 Register(FP)
Estatutos

•Estatutos de Asignación

        <variable> = <expresión>

        es representada por el árbol de ensamblador:

                             Move


                <variable>      <expression>
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
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
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist
Curso prog sist

Más contenido relacionado

La actualidad más candente

Introducción a DSL (Lenguajes Específicos de Dominios) con Python
Introducción a DSL (Lenguajes Específicos de Dominios) con PythonIntroducción a DSL (Lenguajes Específicos de Dominios) con Python
Introducción a DSL (Lenguajes Específicos de Dominios) con PythonJuan Rodríguez
 
Presentacion introduccion
Presentacion introduccionPresentacion introduccion
Presentacion introduccionguesta39fced
 
Estructura basica para_c_
Estructura basica para_c_Estructura basica para_c_
Estructura basica para_c_Jesus22barrera
 
Flex y Bison
Flex y BisonFlex y Bison
Flex y BisonGuiru Xd
 
Manual Basico para Encantadores de Serpientes (Python)
Manual Basico para Encantadores de Serpientes (Python)Manual Basico para Encantadores de Serpientes (Python)
Manual Basico para Encantadores de Serpientes (Python)Fco Javier Lucena
 
Practica 1
Practica 1Practica 1
Practica 1Tensor
 
De Cero A Python En 45 Min
De Cero A Python En 45 MinDe Cero A Python En 45 Min
De Cero A Python En 45 MinMarco Mansilla
 
Clase 1 Curso Introducción a Python 2012
Clase 1 Curso Introducción a Python 2012Clase 1 Curso Introducción a Python 2012
Clase 1 Curso Introducción a Python 2012Nahuel Defossé
 
Estructuras control java-
Estructuras control java-Estructuras control java-
Estructuras control java-SENA
 
Clase 4/4 Curso Introducción a Python 2012
Clase 4/4 Curso Introducción a Python 2012Clase 4/4 Curso Introducción a Python 2012
Clase 4/4 Curso Introducción a Python 2012Nahuel Defossé
 
Especialidad
EspecialidadEspecialidad
Especialidadbecew
 

La actualidad más candente (19)

Introducción a DSL (Lenguajes Específicos de Dominios) con Python
Introducción a DSL (Lenguajes Específicos de Dominios) con PythonIntroducción a DSL (Lenguajes Específicos de Dominios) con Python
Introducción a DSL (Lenguajes Específicos de Dominios) con Python
 
Presentacion introduccion
Presentacion introduccionPresentacion introduccion
Presentacion introduccion
 
Estructura basica para_c_
Estructura basica para_c_Estructura basica para_c_
Estructura basica para_c_
 
Libreria c++
Libreria c++Libreria c++
Libreria c++
 
Flex y Bison
Flex y BisonFlex y Bison
Flex y Bison
 
Introducción a Python
Introducción a PythonIntroducción a Python
Introducción a Python
 
Flex y-bison
Flex y-bisonFlex y-bison
Flex y-bison
 
Act 01
Act 01Act 01
Act 01
 
Assembler
AssemblerAssembler
Assembler
 
Manual Basico para Encantadores de Serpientes (Python)
Manual Basico para Encantadores de Serpientes (Python)Manual Basico para Encantadores de Serpientes (Python)
Manual Basico para Encantadores de Serpientes (Python)
 
Practica 1
Practica 1Practica 1
Practica 1
 
Presentacion Python
Presentacion  Python Presentacion  Python
Presentacion Python
 
De Cero A Python En 45 Min
De Cero A Python En 45 MinDe Cero A Python En 45 Min
De Cero A Python En 45 Min
 
Python (ejercicios)
Python (ejercicios)Python (ejercicios)
Python (ejercicios)
 
Python para principiantes
Python para principiantesPython para principiantes
Python para principiantes
 
Clase 1 Curso Introducción a Python 2012
Clase 1 Curso Introducción a Python 2012Clase 1 Curso Introducción a Python 2012
Clase 1 Curso Introducción a Python 2012
 
Estructuras control java-
Estructuras control java-Estructuras control java-
Estructuras control java-
 
Clase 4/4 Curso Introducción a Python 2012
Clase 4/4 Curso Introducción a Python 2012Clase 4/4 Curso Introducción a Python 2012
Clase 4/4 Curso Introducción a Python 2012
 
Especialidad
EspecialidadEspecialidad
Especialidad
 

Similar a Curso prog sist

sintaxis de los lenguajes de programación
sintaxis de los lenguajes de programaciónsintaxis de los lenguajes de programación
sintaxis de los lenguajes de programaciónbretorio
 
Introducción a los compiladores - Parte 2
Introducción a los compiladores - Parte 2Introducción a los compiladores - Parte 2
Introducción a los compiladores - Parte 2Universidad
 
Estructura de un compilador 2
Estructura de un compilador 2Estructura de un compilador 2
Estructura de un compilador 2perlallamas
 
Generación código intermedio 2
Generación código intermedio 2Generación código intermedio 2
Generación código intermedio 2Humano Terricola
 
Introduccion rapida a SCILAB.pdf
Introduccion rapida a SCILAB.pdfIntroduccion rapida a SCILAB.pdf
Introduccion rapida a SCILAB.pdfcursosrams
 
El papel del analizador sintáctico
El papel del analizador sintácticoEl papel del analizador sintáctico
El papel del analizador sintácticoHector Espinosa
 
Cap4 compiladores
Cap4 compiladoresCap4 compiladores
Cap4 compiladoresCJAO
 
Cap4
Cap4Cap4
Cap4CJAO
 
Actividad 2 Analizador léxico, sintáctico y semántico
Actividad 2 Analizador léxico, sintáctico y semántico Actividad 2 Analizador léxico, sintáctico y semántico
Actividad 2 Analizador léxico, sintáctico y semántico maryr_
 
Taller de Compiladores flx y bsn
Taller de Compiladores flx y bsnTaller de Compiladores flx y bsn
Taller de Compiladores flx y bsnDanielRosero23
 
Compiladores unidad1
Compiladores unidad1Compiladores unidad1
Compiladores unidad1X3025990
 
8 herramientas de procesos de string
8  herramientas de procesos de string8  herramientas de procesos de string
8 herramientas de procesos de stringyimfer1
 
8 herramientas de procesos de string
8  herramientas de procesos de string8  herramientas de procesos de string
8 herramientas de procesos de stringcyberleon95
 
8 herramientas de procesos de string
8  herramientas de procesos de string8  herramientas de procesos de string
8 herramientas de procesos de stringJuan Camilo
 

Similar a Curso prog sist (20)

sintaxis de los lenguajes de programación
sintaxis de los lenguajes de programaciónsintaxis de los lenguajes de programación
sintaxis de los lenguajes de programación
 
Introducción a los compiladores - Parte 2
Introducción a los compiladores - Parte 2Introducción a los compiladores - Parte 2
Introducción a los compiladores - Parte 2
 
Estructura de un compilador 2
Estructura de un compilador 2Estructura de un compilador 2
Estructura de un compilador 2
 
Compiladores
CompiladoresCompiladores
Compiladores
 
Generación código intermedio 2
Generación código intermedio 2Generación código intermedio 2
Generación código intermedio 2
 
Compilador2
Compilador2Compilador2
Compilador2
 
Apuntes scilab
Apuntes scilabApuntes scilab
Apuntes scilab
 
Apuntes scilab
Apuntes scilabApuntes scilab
Apuntes scilab
 
Introduccion rapida a SCILAB.pdf
Introduccion rapida a SCILAB.pdfIntroduccion rapida a SCILAB.pdf
Introduccion rapida a SCILAB.pdf
 
El papel del analizador sintáctico
El papel del analizador sintácticoEl papel del analizador sintáctico
El papel del analizador sintáctico
 
Cap4 compiladores
Cap4 compiladoresCap4 compiladores
Cap4 compiladores
 
Cap4
Cap4Cap4
Cap4
 
Actividad 2 Analizador léxico, sintáctico y semántico
Actividad 2 Analizador léxico, sintáctico y semántico Actividad 2 Analizador léxico, sintáctico y semántico
Actividad 2 Analizador léxico, sintáctico y semántico
 
Unidad1 2 Lenguajes y automatas
Unidad1 2  Lenguajes y automatasUnidad1 2  Lenguajes y automatas
Unidad1 2 Lenguajes y automatas
 
TABLA DE SIMBOLOS
TABLA DE SIMBOLOSTABLA DE SIMBOLOS
TABLA DE SIMBOLOS
 
Taller de Compiladores flx y bsn
Taller de Compiladores flx y bsnTaller de Compiladores flx y bsn
Taller de Compiladores flx y bsn
 
Compiladores unidad1
Compiladores unidad1Compiladores unidad1
Compiladores unidad1
 
8 herramientas de procesos de string
8  herramientas de procesos de string8  herramientas de procesos de string
8 herramientas de procesos de string
 
8 herramientas de procesos de string
8  herramientas de procesos de string8  herramientas de procesos de string
8 herramientas de procesos de string
 
8 herramientas de procesos de string
8  herramientas de procesos de string8  herramientas de procesos de string
8 herramientas de procesos de string
 

Curso prog sist

  • 1. CURSO: PROGRAMACION DE SISTEMAS Dr. Ramón Zatarain Cabada
  • 2. ELEMENTOS DEL CURSO  Programa del Curso..... PDF  Datos del Profesor …. RZC  Contenido del Curso…. Diaps  Proyectos …. Archs  Software y Herramientas
  • 3. Unidad I Introducción a la Programación de Sistemas
  • 4. 1.1 ¿Qué es y que estudia la P. de S. ?  Son los programas que residen en un sistema de computación. Su función es proporcionar al usuario o programador una interfase mas eficiente y practica con relación al hardware de la maquina. La P. de S. estudia como están implementados cada uno de los programas de un Sistema (ver notas).
  • 5. 1.2 Herramientas desarrolladas con la P de S Ejemplos:  Compiladores (javac)  Ensambladores (Masm)  Interpretes (Visual Basic)  Ligadores (Link)  Cargadores  Sistema Operativo (Windows)  Utilerías de Sistemas (Debugger)
  • 6. 1.3 Lenguajes  Naturales (Traductores de Ingles-Español, Ingles-Ruso, etc)  Artificiales (Compiladores de LP como Java, C++, Ada, etc.)
  • 7. 1.4 Traductor y su Estructura  Ensambladores …. (notas)  Compiladores……. (notas)  Intérpretes……...... (notas)
  • 8. 1.5 Generador de Código para Compiladores (Compilador de Compiladores)  Definición: Un compilador de compiladores o generador de Parsers es una utilería para generar el código fuente de un parser, intérprete o compilador a partir de una descripción de lenguaje anotado en la forma de gramática (usualmente BNF) mas código que es asociado con cada una de las reglas de la gramática que debe ser ejecutada cuándo esas reglas sean aplicadas por el parser. Esas piezas de código son algunas veces llamadas rutinas de acciones semánticas ya que ellas definen la semántica de las estructura sintáctica y que es analizada por el parser. Dependiendo del tipo de parser que será generado, las rutinas pueden construir un árbol de parsing (o AST) o generar código ejecutable directamente (notas).
  • 9. Unidad II Introducción al Diseño de Lenguajes de Programación
  • 10.  Visión del Problema.  Consideraciones Preliminares.  Objetivos y filosofías del diseño de los lenguajes de programación.  Diseño detallado.  Caso de estudio.
  • 12. 3.1 Introducción a los Autómatas Finitos y Expresiones Regulares  (ver apuntes de Lenguajes y Autómatas).
  • 13. 3.2 Analizador de Léxico.  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. Ejemplo: TIPO EJEMPLOS ID foo n14 last NUM 73 0 00 515 REAL 66.1 .5 10. 1e67 IF if COMMA , NOTEQ != LPAREN (
  • 14. Analizador de Léxico (cont.) 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.
  • 15. 3.3 Manejo de “Buffers”  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. 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.
  • 16. 3.4 Creación de la Tabla de Símbolos.  Checar notas 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.
  • 17. Ejemplo Ejemplo.- Programa X1; Var X, Y: Integer; Z: Real; Arreglo: Array [1…100] of int Procedure compara (a, b: Integer) Var n, m: integer; Begin ---- ---- End Begin ---- ---- End
  • 18. 3.5 Manejo de Errores de Léxico  Erroresposibles detectados en el análisis de léxico son:  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.  Caracteres inválidos para el alfabeto del el lenguaje.  Longitud de ciertos tokens demasiado larga.
  • 19. 3.6 Generadores de Código Léxico  La construcción de un scanner puede automatizarse por medio de un generador de analizadores de léxico.  El primer programa de este tipo que se hizo popular fue Lex (Lesk, 1975) que generaba un scanner en lenguaje C.  Hoy en día existen muchos programas de este tipo: Flex, Zlex, YooLex, JavaCC, SableCC, etc.
  • 20. Ejemplo: Javacc  Javacc (Java Compiler Compiler) es un generador de scanners y parsers (https://javacc.dev.java.net/).  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.  Se puede usar como pre-procesador de Javacc, a jjtree y a JTB para la construcción del árbol sintáctico.
  • 21. (cont.) USO DE JAVACC Miparser.jj javacc Miparser.jj Miparser.java ParseException.java TokenMgrError.java otros archivos java javac Miparser.java Miparser.class java Miparser <inputfile mensajes
  • 22. (cont.) EJEMPLO DE ESPECIFICACION PARA GENERAR UN SCANNER PARSER_BEGIN(PS1) class PS1 {} PARSER_END(PS1) /* Para la expresión regular de la derecha lo de la izquierda será retornado */ TOKEN: { <IF: "if"> |<#DIGIT: ["0"-"9"]> |<ID: ["a"-"z"] (["a"-"z"]|<DIGIT>)*> |<NUM: (<DIGIT>)+> |<REAL: ((<DIGIT>)+ "." (<DIGIT>)*) | ((<DIGIT>)* "." (<DIGIT>)+)> } SKIP: { <"--" (["a" - "z"])* ("n" | "r" | "rn")> |" " |"t" |"n" |"r" } void Start(): {} { (<IF> | <ID> | <NUM> | <REAL>)* }
  • 24. 4.1 Introducción a las gramáticas Libres de Contexto y Árboles de derivación  El tipo de gramáticas usadas en los LP son llamadas gramáticas de contexto libre, las cuáles, junto con árboles de derivación, fueron estudiadas en el curso de Lenguajes y Autómatas.  Un ejemplo de una gramática para un LP simple es la siguiente: 1) SS;S 2) Sid := E 3) Sprint (L) 4) E id 5) E num 6) E E + E 7) E(S,E) 8) L E 9) L L , E  (ver ejemplo de derivaciones y árboles de parsing)  A continuación veremos un ejemplo de una gramática para Java en formato BNF (liga).
  • 25. (cont.)  Gramática Ambigua. Una gramática es ambigua si puede derivar una oración (cadena) con dos diferentes árboles de parsing. La sig. Gramática es ambigüa: Eid Enum EE*E EE/E EE+E EE-E E(E) ya que tiene dos árboles de parsing para la misma oración (árboles para id:=id+id+id con primera gramática y árboles para 1-2-3 con segunda en sig “slice”).
  • 26. (cont.) S S Id := E Id := E E + E E + E E + E id id E + E Id id id id
  • 27. (cont.) E E E - E E - E E - E 3 1 E - E 1 2 2 3
  • 28. 4.2 Diagramas de Sintaxis  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. Ejemplo en Java (liga)
  • 29. 4.3 Precedencia de Operadores  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:
  • 30. (cont.) SE$ nota: “$” es EOF- marker EE+T EE-T ET T-->T*F TT/F TF Fid Fnum F(E) Esta gramática acepta el mismo lenguaje que la de sección 4.1 pero solo produce un árbol de parsing por oración. Esta gramática no podría producir árboles como los siguientes: X ?U + ?Y ?V * + +
  • 31. 4.4 Analizador Sintáctico  En esta fase se analiza la estructura de la frase del programa.  El parser es el programa que funciona como núcleo del compilador. Alrededor del parser funcionan los demás programas como el scanner, el analizador semántico y el generador de código intermedio. De hecho se podría decir que el parser comienza el proceso de compilación y su primer tarea es pedir al escáner que envíe los tokens necesarios para llevar a cabo el análisis sintáctico, del estatuto, expresión o declaración dentro de un programa.  También el parser llama rutinas del análisis semántico para revisar si el significado del programa es el correcto.  Por ultimo el parser genera código intermedio para los estatutos y expresiones si no se encontraron errores en ellos.
  • 32. (cont.) Existen diferentes técnicas o métodos para realizar un análisis sintáctico “Parsing”. Estas técnicas se dividen en dos tipos:  Descendentes  Ascendentes Las técnicas descendentes realizan su análisis partiendo desde el símbolo inicial de la gramática y realizando derivaciones hasta llegar a producir las hojas o tokens. Por otra parte las técnicas ascendentes realizan su análisis partiendo de las hojas o tokens y mediante una serie de operaciones llamadas reducciones terminan en la raíz o símbolo inicial de la gramática. Por lo general las técnicas descendentes son mas sencillas de implementar que las ascendentes, pero por otra parte son menos eficientes
  • 33. 4.4.1 Analizador descendente (LL). Parsing Predictivo  Algunas gramáticas son sencillas de analizarse sintácticamente usando un algoritmo o método cuyo nombre es recursivo descendente. En esta técnica cada producción de la gramática se convierte en una cláusula de una función recursiva.  Un parser Predictivo es aquel que “ predice ” que camino tomar al estar realizando el análisis sintáctico. Esto quiere decir que el parser nunca regresara a tomar otra decisión ( back tracking ) para irse por otro camino, como sucede en las derivaciones. Para poder contar con un parser predictivo la gramática debe de tener ciertas características, entre ellas la mas importante es que el primer símbolo de la parte derecha de cada producción le proporcione suficiente información al parser para que este escoja cual producción usar. Normalmente el primer símbolo mencionado es un Terminal o token.
  • 34. (cont.)  Esta técnica se utilizó o popularizó en los años 70 a partir del primer compilador de pascal implementado con ella. A continuación ilustraremos esto escribiendo un parser recursivo descendente para la siguiente gramática: S  if E then S else S S begin S L S print E L end L ; S L E num = num Nuestro Parser tendría 3 métodos (uno por cada producción)
  • 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. (cont.)  Un parser predictivo que examina la entrada de izquierda a derecha (left-to-right) en un paso y realiza su derivación por la izquierda es llamado “Parser LL”.  Cuando el parser solo necesita “mirar” el siguiente token para hacer su función (llokahead(1)), recibe el nombre de Parser LL(1).  Un parser podría necesitar “mirar” K tokens por adelantado para tomar desiciones. En este caso recibe el nombre de parser LL(K). Ejemplo: (parte de una gramática) IF-STM  if EXP then STM else STM |  if EXP then STM
  • 37. Eliminación de Recursión por la izquierda  Suponer que queremos construír un Parser predictivo para la gramática de sección 4.3. SE$ EE+T EE-T ET T-->T*F TT/F TF Fid Fnum F(E) Producciones como E  E + T contienen recursión por la izquierda. Parsers descendentes no pueden manejar recursión por la izquierda en una gramática. Para eliminar este tipo de recursión utilizamos la siguiente transformación:
  • 38. (cont.) En general, si tenemos producciones X  X γ y X  α, donde α no comience con X podemos aplicar la siguiente transformación: X  Xγ1 X  α1X’ X  Xγ2 X  α2X’ X  α1 X’  γ1X’ X  α2 X’  γ2X’ X’  λ
  • 39. (cont.)  Aplicando la transformación a la gramática anterior, obtenemos la nueva equivalente gramática (sin recursión por la izquierda): S  E$ E  T E’ E’  + T E’ E’  - T E’ E’  λ T  F T’ T’  * F T’ T’  / F T’ T’  λ F  id F  num F  (E)
  • 40. Factorización por la izquierda  Otro problema en una gramática ocurre cuando dos producciones para el mismo no terminal comienza con el mismo símbolo. Por ejemplo: IF-STM  if EXP then STM else STM IF-STM  if EXP then STM En dicho caso podemos factorizar por la izquierda la gramática. El resultado sería el sig: IF-STM  if EXP then STM X X λ X  else IF-STM las producciones anteriores facilitarán el trabajo del parser predictivo.
  • 41. 4.4.2 Analizador ascendente(LR y LALR).  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.  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).  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.
  • 42. (cont.)  Un Parser LR(K) está compuesto de:  La cadena de entrada  Una pila  Una tabla de Parsing LR  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
  • 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. Ejemplo: Parser LR(K) TABLA DE PARSING id num print ; , + := ( ) $ S E L 1 S4 s7 g2 2 s3 a 3 S4 s7 g5 4 s6 5 r1 r1 r1 6 S20 s10 s8 g11 7 s9 8 S4 s7 g12 9 S20 s10 g15 g14 10 r5 r5 r5 r5 r5 11 r2 r2 s16 r2 12 s3 s18 13 r3 r3 r3 14 s19 s13 15 r8 r8 16 S20 s10 s8 g17 17 r6 r6 s16 r6 r6 18 S20 s10 s8 g21 19 S20 s10 s8 g23 20 r4 r4 r4 r4 r4 21 s22 22 r7 r7 r7 r7 r7 23 r9 s16 r9
  • 45. Ejemplo (cont.): PILA ENTRADA ACCION 1 a:=7;B:=c+(d:=5+6,d)$ shift 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 1 id4 := 6 E11 ;B:=c+(d:=5+6,d)$ reduce S->id:=E . . . . . . . . . . . . . . . . . . . . . 1 S2 $ accept
  • 46. Parsing LALR  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.  Esta técnica fue inventada por DeRemer en 1971.  Casi cualquier construcción sináctica de un LP puede ser expresado de manera conveniente por una gramática LALR.  Generadores de Parsers famosos como YACC (Yet Another Compiler-Compiler-Johnson) producen un parser LALR.  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.
  • 47. 4.5 Administración de tablas de símbolos.  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.  Esto ocurre al detectarse las declaraciones en el programa fuente.  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).
  • 48. 4.6 Manejo de errores sintácticos y su recuperación.  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.  ¿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.  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.
  • 49. (cont.)  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.  Esto se puede realizar por medio de borrar, reemplazar o insertar tokens.  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.
  • 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. 4.7 Generadores de código para analizadores sintácticos  Como se vio al final del capítulo 3, Javacc es un generador de scanners y de parsers (https://javacc.dev.java.net/).  Javacc produce un parser del tipo descendente (recursivo) o LL(k) donde k (lookahead) puede ser cualquier número entero positivo.  El parser producido puede correr en cualquier plataforma que cuente con el compilador de Java.  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.
  • 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 : { "" | "t" | "n" | "r" }
  • 53. (cont. Ejemplo) TOKEN : { void Program() : {} <INT: "INT"> { S() <EOF> } | <IF: "if"> | <THEN: "then"> void S() : | <ELSE: "else"> {} | <BEGIN: "begin"> { | <PRINT: "print"> <BEGIN> S() L() | <END: "end"> | <PRINT> E() | LOOKAHEAD(12) "if" E() "then" S() "else" S() | <SEMI: ";"> | "if" E() "then" S() | <EQUAL: "="> } | <ID: (["a"-"z"]|["A"-"Z"]) (["a"-"z"]|["A"-"Z"]|["0"-"9"])* > void L() : } {} { <END> | <SEMI> S() L() } void E() : {} { <ID> <EQUAL> <ID> }
  • 55. 5.1 Analizador semántico  Un compilador no solo tiene que revisar la sintaxis de código fuente, si no también la semántica de este.  Al igual que en los lenguajes naturales (español, ingles, etc.) en los lenguajes de programación existen reglas semánticas para definir el significado de los programas, estatutos, expresiones, etc.  Por ejemplo un error semántico es usar (en pascal ó java) un identificador que no fue anteriormente declarado.  Otro ejemplo de error semántico en un programa es cuando este es compilado y y no se detectan errores pero el momento de ser ejecutado este programa no funciona correctamente.
  • 56. 5.2 Verificación de tipos en expresiones.  Cuando mezclamos diferentes tipos en una misma expresión o que llamamos una rutina que no existe existe un error semántico.  Una de las funciones del analizador smántico es verificar que los tipos de una expresión sean compatibles entre si.  Para hacer lo anterior el compilador cuenta con información de los atributos (tipos, tamaño, número de argumento, etc.) de los identificadores en una tabla de símbolos.
  • 57. 5.3 Conversión de tipos.  Algunas veces los tipos de una expresión o estatuto son diferente.  Por ejemplo en la asignación, a = b * c; el tipo del resultado de evaluar b*c es diferente al de el identificador a.  El compilador algunas veces con ciertos diferentes tipos puede hacer una conversión interna en forma implícita para solucionar el problema. Otras veces el programador explícitamente es el que hace la conversión (casting). Ejemplo: float dinero; int cambio; dinero = (float) cambio;
  • 58. 5.4 Acciones agregadas en un Analizador sintáctico descendente (top-down).  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.  Cada símbolo terminal y noterminal puede asociarse con su propio tipo de valor semántico. Por ejemplo en la siguiente gramática para YACC de una calculadora simple, el tipo asociado con exp e INT podría ser int: %token INT PLUS MINUS TIMES UMINUS %start exp %left PLUS MINUS %left TIMES %left UMINIS exp: INT | exp PLUS exp | exp MINUS exp | exp TIMES exp 1 MINUS exp %prec UMINUS
  • 59. (cont.)  Los otros tokens no necesitarían tener un valor.  Por otra parte el tipo asociado a un token debe por supuesto coincidir con el tipo de token que el scanner retorne.  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.
  • 60. Recursivo-descendente  En un parser recursivo-descendente, las acciones semánticas son los valores retornados por las funciones de parsing, o los efectos laterales de esas funciones o ambos.  Por cada símbolo terminal y noterminal, asociamos un tipo (desde el lenguaje de implementación del LP del compilador) de valor semántico representando frases derivadas desde ese símbolo.  El siguiente programa es un intérprete recursivo descendente para una parte de la gramática en la cual eliminamos la recursión por la izquierda (por conveniencia la volvemos a mostrar):
  • 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. Interprete Acciones semánticas class Token2 { int kind; Object val; Token2(int k, Object v) int T() {switch(tok.kind) { { case ID: kind=k; case NUM: val=v; case LPAREN: return Tprime(F()); } default: print("2 esperaba ID, NUM o parent izq"); } //skipto(T_follow); final int EOF=0, ID=1, NUM=2, PLUS=3, return 0; MINUS=4,LPAREN=5, RPAREN=6, TIMES=7; }} int lookup(String id) { …. } int F_follow[] = {PLUS,TIMES,RPAREN,EOF}; int Tprime(int a) {switch (tok.kind) { case TIMES: eat(TIMES); return Tprime(a*F()); int F() {switch(tok.kind) { case PLUS: case ID: int i=lookup((String)(tok.val));advance()return i; case RPAREN: case NUM: int i=((integer)(tok.val)).intVal(); case EOF: return a; advance();return i; default: print("3 esperaba ID, NUM o parent izq"); case LPAREN: eat(LPAREN); //skipto(T_follow); int i=E(); return 0; eatOrSkipTo(RPAREN,F_follow); }} return i; case EOF: void eatOrSkipTo(int expected, int[] stop) { default: print("1 esperaba ID,NUM, o parent izq"); if (tok.kind==expected) //skipto(F_follow); return 0; eat(expected); }} else {print("4 esperaba ID, NUM o parent izq"); //skipto(stop);} int T_follow[]= {PLUS,RPAREN,EOF}; }
  • 63. Parser Automáticamente generado  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. Ejemplo: void Start(): Int Term(): { int i; } { int a,i; } { i=Exp() <EOF> {System.Out.println(i);} { a=factor() } ( “*” i=Factor() { a=a*i;} Int Exp(): | “/” i=Factor() {a=a/i;} { int a,i; } )* { a=Term() { return a; } ( “+” i=Term() {a=a+i;} } | “-” i=Term() {a=a-i;} Int Factor(): )* { Token t; int i; } { return a; } { t=<IDENTIFIER> { return lookup(t.image); } } | t=<INTEGER_LITERAL> {return Integer.parseInt(t.image); } | “(“ i=Exp() “)” {return i; } }
  • 64. Árboles de Parsing Abstractos  Para mejorar la modularidad del compilador, es recomendable separar detalles de la sintaxis con detalles de la semántica (chequeo de tipos y traducción a código máquina).  Una forma de hacerlo es producir un árbol de sintaxis abstracta (una forma condensada de árbol de parsing útil para representar construcciones del LP).  Por ejemplo la producción S  if B then S1 else S2 pudiera aparecer en un arbol sintáctico como: Árbol Est-if Árbol If-then-else De sintáctico parsing If Exp ( Exp ) Est B S1 S2 En un árbol sintáctico, los operadores y las palabras claves (reservadas) no aparecen como hojas, sino que están asociadas con el nodo interior que sería el padre de esas hojas en el arbol de parsing.
  • 65. (cont.)  Otro ejemplo es en el árbol de parsing: L E + T T * F F F 4 8 2 + Cuyo árbol sintáctico abstracto sería: * 8 2 4
  • 66. Ejemplo:  La gramática siguiente nos muestra una sintaxis abstracta de un lenguaje para expresiones: EE+E EE-E EE*E EE/E Eid Enum  Esta gramática es impráctica para un parser ya que es ambigua pues no tiene precedencia de operadores.  Sin embargo, esta gramática no es para el parser. El analizador semántico podría usarla el cual no se molesta por la ambiguedad puesto que ya tiene su arbol.
  • 67. Árboles de Sintaxis en Java  EnJava 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.
  • 68. Programa de clases para Exp public abstract class ExpCh4 { public abstract int eval(); } class PlusExp extends ExpCh4 { private ExpCh4 e1,e2; class DivideExp extends ExpCh4 { public PlusExp(ExpCh4 a1, ExpCh4 a2) private ExpCh4 e1,e2; {e1=a1; e2=a2;} public DivideExp(ExpCh4 a1, ExpCh4 a2) public int eval() { {e1=a1; e2=a2;} return e1.eval()+e2.eval(); public int eval() { } return e1.eval()/e2.eval(); } } class MinusExp extends ExpCh4 { } private ExpCh4 e1,e2; class Identifier extends ExpCh4 { public MinusExp(ExpCh4 a1, ExpCh4 a2) private String f0; {e1=a1; e2=a2;} public Identifier(String n0) {f0=n0;} public int eval() { public int eval() { return e1.eval()-e2.eval(); return (7); //return lookup(f0); } } } } class TimesExp extends ExpCh4 { class IntegerLiteral extends ExpCh4 { private ExpCh4 e1,e2; private String f0; public TimesExp(ExpCh4 a1, ExpCh4 a2) public IntegerLiteral(String n0) {f0=n0;} {e1=a1; e2=a2;} public int eval() { public int eval() { return (4); return e1.eval()*e2.eval(); //return Integer.parseInt(f0); } } } }
  • 69. (cont.)  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. S  E$ ETE’ E’  + T E’ E’  - T E’ E’  λ T  F T’ T’  * F T’ T’  / F T’ T’  λ F  id F  num F  (E)  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.
  • 70. Gramática con acciones semánticas para árboles sintácticos PARSER_BEGIN(InterSinTree) class InterSinTree {} PARSER_END(InterSinTree) TOKEN: { ExpCh4 Exp(): { ExpCh4 e1,e2; } <#DIGIT: ["0"-"9"]> { e1=Term() |<ID: ["a"-"z"] (["a"-"z"]|<DIGIT>)*> ("+" e2=Term() { e1=new PlusExp(e1,e2);} |<INTEGER_LITERAL: (<DIGIT>)+> |"-" e2=Term() { e1=new MinusExp(e1,e2);} )* } { return e1;} } SKIP: { ExpCh4 Term(): <"--" (["a" - "z"])* ("n" | "r" | "rn")> { ExpCh4 e1,e2; } |" " { e1=Factor() |"t" ("*" e2=Factor() { e1=new TimesExp(e1,e2);} |"n" |"/" e2=Factor() { e1=new DivideExp(e1,e2);} |"r" )* } { return e1;} } ExpCh4 Start(): ExpCh4 Factor() : { ExpCh4 e; } { Token t; ExpCh4 e; } { e=Exp() <EOF> { (t=<ID> {System.out.println(e.eval()); return e; } {return new Identifier(t.image); } | } t=<INTEGER_LITERAL> {return new IntegerLiteral(t.image); } | "(" e=Exp() ")" {return e; }) }
  • 71. VISITADORES  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.
  • 72. (cont.) 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.
  • 73. Sintáxis Abstracta para MiniJava  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: public class ExpList { private Vector list; public ExpList() { list=new vector(); } Public void addElement (Exp n) { list.addElement(n); } public Exp elementAt(int i) { return (exp)list.elementAt(i); } public int size() { return list.size(); } }
  • 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. (cont. figura) Abstract class Exp And(Exp e1, Exp e2) LessThan(Exp e1, Exp e2) Plus(Exp e1, Exp e2) Minus(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. Arbol Sintáctico  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.
  • 77. public interface Visitor { public void visit(Program n); public void visit(MainClass n); public void visit(ClassDeclSimple n); public void visit(ClassDeclextends n); public void visit(VarDecl n); public void visit(MethodDecl n); public void visit(Formal n); public void visit(IntArrayType n); public void visit(BooleanType n); public void visit(IntegerType n); public void visit(IdentifierType n); public void visit(Block n); Visitador public void visit(If n); public void visit(While n); MiniJava public void visit(Print n); public void visit(Assign n); public void visit(ArrayAssign n); public void visit(And n); public void visit(LessThan n); public void visit(Pluss n); public void visit(Minus n); public void visit(Times n); public void visit(ArrayLoockup n); public void visit(ArrayLength n); public void visit(Call n); public void visit(IntegerLiteral n); public void visit(True n); public void visit(False n); public void visit(IdentifierExp n); public void visit(This n); public void visit(NewArray n); public void visit(NewObject n); public void visit(Not n); public void visit(Identifier n); }
  • 78. (cont. Arbol Sintáctico)  Podemos construir un árbol sintáctico usando expresiones new anidadas. Por ejemplo el árbol sintáctico para el estatuto MiniJava: x = y.m(1,4+5); usaría el siguiente código: ExpList el= new ExpList(); el.addElement(new IntegerLiteral(1)); el.addelement(new Plus(new IntegerLiteral(4), new IntegerLiteral(5))); Statement s = new Assign(new Identifier “x”), new Call(new identifierExp(“y”), new Identifier(“m”), el));
  • 79. 5.5 Pila semántica en un analizador sintáctico ascendente (bottom-up).  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.  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.  Estos atributos estarían ligados a la correspondiente producción en la tabla de parsing (consultar sección 5.3 del libro de “Aho, Ullman, Sethi” para ver mas detalles de la implementación).
  • 80. 5.6 Administración de la tabla de símbolos  El análisis semántico conecta las definiciones de las variables con sus usos, checa que cada expresión tenga un tipo correcto y traduce la sintaxis abstracta a una representación mas simple para generar código máquina.  Esta fase es caracterizada por el mantener la tabla de símbolos (también llamada “environment”) la cual mapea identificadores con sus tipos y localidades.  Cada variable local en un programa tiene un ámbito (scope) dentro del cual es visible. Por ejemplo, en un método MiniJava m, todos los parámetros formales y variables locales declarados en m son visibles solo hasta que finalice m.
  • 81. (cont.)  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.
  • 82. Ejemplo: 1 Class C { 2 int a, int b; int c; 3 public void m() { 4 System.out.println(a+c); 5 int j=a+b; 6 String a=“hello”; 7 System.out.println(a); 8 System.out.println(j); 9 System.out.println(b); 10 } 11 } 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. Implementación de la Tabla  Existendos 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.
  • 84. Múltiple Tablas de Símbolos  Enalgunos 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). Structure M = struct Package M; structure E = struct class E { val a= 5; static int a=5; end } structure N = struct Class N { val b=10 static int b=10; val a=E.a+b static int a=E.a+b; end } structure D = struct Class D { val d=E.a+N.a static int d=E.a+N.a; end } end
  • 85. (cont.)  Al anlizar los 2 programas anteriores, sea z0 el ambiente base conteniendo funciones predefinidas, y sea z1={aint} z2={Ez1} z3={bint,aint} z4={Nz3} z5={dint} z6={Dz5} z7=z2+z4+z6
  • 86. (cont.)  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}.  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.
  • 87. TABLAS DE SIMBOLOS EN LENGUAJES IMPERATIVOS  Un programa grande puede contener miles de distintos identificadores. Esto hace que la búsqueda en la tabla (loock up) tenga que ser eficiente.  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”.
  • 88. (cont.)  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.
  • 89. SIMBOLOS  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.  El módulo símbolo implementa los símbolos y tiene estas propiedades:  Comparar símbolos por igualdad o por mayor es rápido (comparación por apuntador o por entero).  Extraer una llave “hash” de tipo entero es rápido
  • 90. (cont.)  Los ambientes son implementados en la clase Symbol.Table como Tables mapeando Symbols a ligados (bindings).  Para eso se manejan ligaduras para diferentes propósitos en el compilador – ligadura para tipos, para variables, para funciones, etc.  Entonces, una ligadura es un Objeto.  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.  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.
  • 91. (cont.)  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.  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).  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.
  • 92. Chequeo de Tipos en MiniJava  ¿Con que se llena una tabla de símbolos? Esto es, ¿Qué es la ligadura o “binding”?  Para realizar el chequeo de tipos de programas MiniJava, la tabla de símbolos debe contener toda la información declarada:  Cada nombre de variable y nombre de parámetro formal debe ser ligado a su tipo.  Cada nombre de método debe ser ligado a sus parámetros, tipo de resultado y variables locales.  Cada nombre de clase debe ser ligado a su variable y declaraciones de métodos.  Liga a página con mas información.
  • 93. (cont.)  Por ejemplo, considere la siguiente figura, que muestra un programa y su tabla de símbolos. PARAMS p int Class B { q int C f; int [ ] j; int q; public int start(int p, int q) { FIELDS int ret; int a; f C LOCALS /* …… */ j int [ ] ret int return ret; q int a int B } C METHODS public boolean stop(int p) { start int /* ….. */ stop bool PARAMS return false; p int } } …… LOCALS Class C { /* ….*/ }
  • 94. (cont.)  Los tipos primitivos en MiniJava son int y boolean; todos los otros tipos son arreglo de enteros o nombres de clases.  Por simplicidad todos los tipos son “string”, en lugar de símbolos; esto nos permite checar igualdad de tipos por medio de comparación de “strings”.  El chequeo de tipos de un programa MiniJava ocurre en dos fases. Primero, construimos la tabla de símbolos y después checamos los tipos de los estatutos y las expresiones. Lo hacemos en dos fases porque en MiniJava (igual que en Java) las clases son mutuamente recursivas.
  • 95. (cont.)  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.  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.  El método visitador checa si la variable ya fue declarada anteriormente.
  • 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. (cont.)  La segunda fase del checador de tipos puede ser implementada por un visitador que cheque tipos de todas los estatutos y expresiones.  El tipo del resultado de cada método visitador es String, que representa los tipos de MiniJava.  La idea es que cuando el visitador visita una expresión, entonces retorna el tipo de esa expresión.  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).
  • 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. 5.7 Manejo de errores semánticos.  Cuando el checador de tipos detecta un error de tipos o un identificador no declarado, debe imprimir el mensaje de error y continuar.  Esto debido a que normalmente el programador prefiere que le describan todos los errores posibles del programa fuente.  Esto quiere decir, que si un error de tipos es encontrado, no debe producirse un programa objeto por parte del compilador.  Así, las siguientes fases no deben ejecutarse.  Hasta esta etapa (chequeo de tipos), la parte del compilador se conoce con el nombre de “front End”.
  • 100. REGISTROS DE ACTIVACION  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).  Diferentes invocaciones a la función pueden existir a la vez, y cada invocación tiene su propia “instanciación” de variables.
  • 101. (cont.)  En el siguiente método de Java 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. (cont.)  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.
  • 103. MARCOS DE PILA  Debido a que se trabaja con bloques de datos por función un “push” y “pop” no funciona.  Entonces la pila es tratada como si fuera un gran arreglo, con un registro especial- el “stack pointer” que apunta a una localidad.  Todas las localidades después del apuntador son basura y todas las que están antes están asignadas.  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.
  • 104. (cont.)  El diseño de la estructura de los marcos es de acuerdo con la arquitectura y el LP que se compila.  Aunque normalmente el constructor de la arquitectura define un diseño de marco standard para todos los compiladores para esa arquitectura.
  • 105. Ejemplo: Un marco de pila Direcciones de Memoria mas altas Argumentos Argumento n Marco anterior de entrada … … Argumento 1 Apuntador Liga estática Del marco Variables locales Marco Dirección retorno actual Temporales Registros salvados Argumento m Argumentos … De salida … Argumento 1 Apuntador Liga estática De pila Marco siguiente
  • 106. Marco de Pila  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).  Cuando la función actual llama otras funciones, puede usar el espacio de los argumentos de salida para pasar parámetros.  La dirección de retorno es creada por la instrucción CALL.  Las variables locales también tienen su espacio.  Las variables mantenidas en registros algunas veces son salvadas a memoria.
  • 107. El Apuntador de Marco (FP)  Suponer que una función g(…) llama la función f(a1,…an). 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.  El viejo SP se convierte en el actual FP y el viejo FP es salvado en el marco.  Cuando FP termina, solo copia FP de regreso a SP y regresa el valor viejo salvado de FP.  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.
  • 108. Registros  Por eficiencia, es importante mantener las variables locales, resultados intermedios y otros valores en registros en lugar de la pila de marcos.  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.  ¿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.
  • 109. Pase de Parámetros  Estudios actuales han mostrado que raramente una función pasa mas de 4 parámetros.  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.
  • 110. Direcciones de Retorno  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.  En máquinas modernas la dirección de retorno es pasada a un registro en lugar de la memoria.  En funciones “hoja” la dirección no necesita ponerse en la pila.
  • 111. Registros vs. Memoria  Registros siempre deben usarse en asignación a menos que:  La variable sea pasada por referencia  La variable es accesada por una función anidada dentro de la función actual.  El valor es demasiado grande para un registro.  La variable es un arreglo, donde es necesario realizar aritmética de direcciones.  El registro que contiene la variable es necesitado para otros propósitos.  Existen demasiadas variables locales y valores temporales
  • 112. Ligas Estáticas (Static Links)  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).  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).
  • 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. Ligas Estáticas (cont.)  Existen varios métodos para solucionar lo anterior:  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.  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”.  A continuación describimos el método de liga estática para el ejemplo de la diapositiva anterior.
  • 115. (cont.)  Línea 21: prettyprint llama show, pasando el apuntador del marco del propio prettyprint como una liga estática de show.  Línea 10: show guarda su liga estática (la dirección del marco de prettyprint) dentro de su propio marco.  Línea 15: show llama indent, pasando su propio apuntador de marco como liga estática de indent.  Línea 17: show llama a show,pasando su propia liga estática (no su propio apuntador de marco) como la liga estática.  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).  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.  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.
  • 116. Unidad VI Generación de Código Intermedio
  • 117. 6.1 Lenguajes intermedios.  El código intermedio en una estructura de código cuya complejidad está entre un código fuente en un lenguaje de alto nivel y el código máquina. Código fuente  Código intermedio  Código Objeto  Un compilador produce primero un código intermedio, pues producir directamente el código objeto resulta sumamente complicado e ineficiente.
  • 118. (cont.) Ventajas de producir código intermedio:  Más fácil de producir código objeto después, si se parte de un código intermedio.  Facilita y hace eficiente la optimización de código (algunos tipos de optimización).  El código intermedio es transportable (puede usarse en diferentes arquitecturas) ya que no hace referencia a componentes de la máquina.
  • 119. 6.2 Notaciones.  Infija. Es la notación habitual. El orden es primer operando, operador, segundo operando. Ejemplo: a/b/c La notación infija tiene el problema de que en expresiones con más de un operador existe ambiguedad sobre cual es el orden de evaluación. Por ejemplo, la expresión 8/4/2 se puede interpretar como (8/4)/2 o bien como 8/(4/2). Las otras notaciones (prefija y postfija) no sufren este problema.
  • 120. (cont.)  Postfija. El orden es primer operando, segundo operando, operador. Ejemplo: La expresión X+Y-X*Y en notación Postfija es XY+XY*- Por lo general la notación postfija se emplea en máquinas de pilas ya que la pila facilita la ejecución. Al analizar una notación postfija de izquierda a derecha, cada vez que se detecta un operando se mete a la pila. La ocurrencia de un operador con ‘m' operandos significa que el enésimo operando estará m-n posiciones por debajo del tope de la pila. Después se sacan los operandos de la pila y se mete el resultado de la operación. Por ejemplo, suponer que X=1 y Y=2. Las operaciones serían: push 1 (meter 1) push 2 ' + ' requiere de 2 operandos, se sacan, se suman y se mete el resultado (3) push 1 push 2 ' * ' se mete el resultado(2) ' - ' se mete el resultado (1)  Notación prefija: El orden es operador, primer operando, segundo operando.
  • 121. 6.3 Representación de código intermedio.  Existen muchas clases de representaciones intermedias. Algunas de las mas comunes son:  Notación polaca.  Código P (Pascal).  Triples y Cuadruples.  Bytecodes (Java)  MSIL (C#)
  • 122. Notación Polaca  También conocida como notación postfija. Se utiliza como se dijo anteriormente en máquinas de Pila. Ejemplos: Pascal Notación Polaca a+b-c ab+c- a+b*c abc*+ a+b*c+d abc*+d+
  • 123. Código P  Se usó como código intermedio y objeto en las primeras implementaciones de Pascal.  El código P era interpretado en una máquina abstracta.  Algunas implementaciones de Basic y Pascal usan código P el cual después es traducido a código nativo por un compilador “Just-in-Time”.  La máquina P está orientada a usarse en una pila (stack-oriented).
  • 124. Ejemplo: Insn. Stack Stack Description before after adi i1 i2 i1+i2 add two integers Adr r1 r2 r1+r2 add two reals dvi i1 i2 i1/i2 integer division ldci i1 i1 load integer constant mov a1 a2 move not b1 ~b1 boolean negation
  • 125. Triples y Cuadruplos (Código de 3 Direcciones)  Ha sido uno de los mas populares. Sus instrucciones constan de 3 direcciones o registros para 2 argumentos u operandos y el resultado. Su formato es: resultado:= argumento1 operador argumento2 Donde resultado, argumento1 y argumento2 pueden ser constantes, identificadores y variables temporales definidos por el compilador mientras que operador representa una operación arbitraria. Esta forma se denomina Cuádruplo.
  • 126. (cont.) EJEMPLO: Z:= X + Y – X * Y ADD X Y VAR1 MUL X Y VAR2 SUB VAR1 VAR2 VAR3 STORE VAR3 Z
  • 127. (cont.)  EJEMPLO: (Estructure de Control): If (a==b) a=0; else a=1;  En cuadruplos tendríamos: 1 - A B t1 2 JnZ t1 5 3 = 0 A 4 JP 6 5 = 1 A 6
  • 128. (cont.)  En el código de 2 direcciones se evita el uso se variables temporales. El formato es: Operador argumento1 argumento2 EJEMPLO: Z = X + Y - X * Y 1. ADD X Y 2. MUL X Y 3. SUB (1) (2) 4. STORE (3) (Z) Donde los números entre paréntesis representan apuntadores a la secuencia de operaciones de 2 direcciones.
  • 129. Java Bytecodes  El bytecode es un código intermedio más abstracto que el código máquina. Habitualmente viene a ser un archivo binario que contiene un programa ejecutable similar a un módulo objeto o código máquina producido por el compilador.  El bytecode recibe su nombre porque generalmente cada código de operación tiene una longitud de un byte, si bien la longitud del código de las instrucciones varía.  Cada instrucción tiene un código de operación entre 0 y 255 seguido de parámetros tales como los registros o las direcciones de memoria. Esta sería la descripción de un caso típico, si bien la especificación del bytecode depende ampliamente del lenguaje.
  • 130. (cont.)  Como código intermedio, se trata de una forma de salida utilizada por los implementadores de lenguajes para reducir la dependencia respecto del hardware específico y facilitar la interpretación.  Menos frecuentemente se utiliza el bytecode como código intermedio en un compilador. Algunos sistemas, llamados traductores dinámicos o compiladores just-in-time (JIT) traducen el bytecode a código máquina inmediatamente antes de su ejecución para mejorar la velocidad de ejecución.  Los programas en bytecode suelen ser interpretados por un intérprete de bytecode (en general llamado máquina virtual, dado que es análogo a un ordenador).
  • 131. (cont.)  Su ventaja es su portabilidad: el mismo código binario puede ser ejecutado en diferentes plataformas y arquitecturas. Es la misma ventaja que presentan los lenguajes interpretados.  Sin embargo, como el bytecode es en general menos abstracto, más compacto y más orientado a la máquina que un programa pensado para su modificación por humanos, su rendimiento suele ser mejor que el de los lenguajes interpretados.  A causa de esa mejora en el rendimiento, muchos lenguajes interpretados, de hecho, se compilan para convertirlos en bytecode y después son ejecutados por un intérprete de bytecode.  Entre esos lenguajes se encuentran Perl, PHP y Python. El código Java se suele trasmitir como bytecode a la máquina receptora, que utiliza un compilador just-in-time para traducir el bytecode en código máquina antes de su ejecución.
  • 132. La Máquina Virtual de Java (JVM)  La siguiente liga nos lleva a la información de lo que es la JVM y mas sobre Java bytecodes.
  • 133. Árboles de Ensamblador  Un paso antes de generar Ensamblador Real  Permite mas fácil optimización de código  Primero vamos a estudiar la implementación de asignación de memoria en el código generado (stack y heap).
  • 134. Implementación de variables  En Java las variables locales son almacenadas en la pila y en registros.  Las variables de arreglos y de clases son almacenadas en el heap.  Registros de Activación (RA).  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.
  • 135. Por ejemplo, la función: Int foo() { int a; int b; /* body of foo */ } Tendría el registro de activación a Existen dos apuntadores. FP que FP apunta al inicio del RA actual y SP que b apunta a la primera localidad vacía. El Registros valor retornado de una función es salvados puesto en un registro especial en lugar de la pila. Cuando una función es SP 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.
  • 136. (cont.)  Como el FP siempre apunta al comienzo del RA actual, podemos acceder a las variables locales usando un offset desde el FP.  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.
  • 137. Formato completo de RA Input Parameter n Previous Activation Record … Input Parameter 2 Input Parameter 1 FP Local Variables Current Activation Record Saved Registers SP
  • 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. En label A de el programa, la pila de RA se miraría de la forma siguiente u FP v Activation Record for Saved Registers main SP 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.
  • 140. En etiqueta B los RA se mirarían así: u Activation Record v for main Saved Registers b a w x Activation Record for bar Saved Registers d c y FP Activation Record z for foo Saved Registers SP
  • 141. (cont.)  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).  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.
  • 142. Ensamblador Abstracto  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.  Existen cinco diferentes tipos de árboles de expresiones:  Constante. Consiste de un valor de la constante (solo enteros).  Registro. Una cadena que especifica el registro.  Operador. Dos árboles de expresión para los operandos, y el operador: +,-,*,/,<,>,<=,>=,=,&&,||,!.
  • 143. (cont.)  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: CallExpresion(“foo”) Constant(3) Constant(4) 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.
  • 144. Memoria. Solo se denota la dirección de memoria. Ejemplo: Memory Constant(1006) 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 Operator(-) Register(FP) Constant(4)
  • 145. Existen 8 tipos de árboles de estatutos:  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.  Label (Etiqueta). Se usan como destinos de “jumps” y “calls”.  Jump. Saltos incondicionales contienen una etiqueta.  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.  Sequential. Tiene dos subárboles. Representa la ejecución del izquierdo seguido del derecho.  CallStatement. Contienen una árbol de etiqueta, y un árbol expresión de cada uno de los parámetros actuales. “Calls” representan “void function calls”.  Empty. Son “no-ops” y son removidos cuando se traduce a ensamblador real.  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”.
  • 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. X=1; Move Memory Constant(1) Register(FP)
  • 148. y = a * b; Move Memory Operator(*) Memory Memory Operator (-) Operator (+) Operator (+) Register(FP) Constant(4) Register(FP) Constant(4) Register(FP) Constant(8)
  • 149. Y++; Move Memory Operator(+) Memory Constant(1) Operator (-) Operator (-) Register(FP) Constant(4) Register(FP) Constant(4)
  • 150. bar(y,x+1,a); CallStatement(“bar”) Operator (+) Memory Memory Operator (-) Memory Constant(1) Operator (+) Register(FP) Constant(4) Register(FP) Register(FP) Constant(4)
  • 151. x=function(y+1,3); Move Memory Callexpression(“function”) Register(FP) Operator(+) Constant(3) Memory Constant(1) Operator(-) Register(FP) Constant(4)
  • 152. if (x > 2) z = true; else z = false; Sequential Sequential ConditionalJump(“iftrue”) Sequential Operator(>) Sequential Jump(“ifend”) Move Sequential Memory Constant(2) Label(“iftrue”) Label(“ifend”) Memory Constant(0) Move Register(FP) Operator(-) Memory Constant(1) Operator(-) Register(FP) Constant(8) Register(FP) Constant(8)
  • 153. Creación de Ensamblador Abstracto  Variables  Variables base void foo() { int x; int y; /* Body of foo */ }  La variable x y y se representarían con los árboles: Memory Memory Operator(-) Register(FP) Register(FP) Constant(4)
  • 154. Variables Arreglo: Son almacenadas en el heap, y usando un apuntador a la base del arreglo el cual se almacena en la pila. La siguiente función o método void foo arrayallocation() { int x; int A[]; int B[]; A = new int [5]; B = new int [5]; /* Body of function */ }
  • 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[0] FP A[1] A B A[2] A[3] A[4] Saved Registers B[0] B[1] B[2] SP B[3] B[4]
  • 156. ¿Cómo debemos representar el arreglo A[3] Memory Operator(-) Memory Operator(*) Operator(-) Constant(WORDSIZE) Constant(3) Register(FP) Constant(WORDSIZE)
  • 157. ¿ Y A[x] ? Memory Operator(-) Memory Operator(*) Operator(-) Constant(WORDSIZE) Memory Constant(WORDSIZE) Register(FP) Register(FP)
  • 158. ¿ Y para A[B[2]] ? Memory Operator(-) Memory Operator(*) Operator(-) Constant(WORDSIZE) Memory Operator(-) Register(FP) Constant(WORDSIZE) Operator(*) Memory Constant(WORDSIZE) Operator(-) Constant(2) Register(FP) Constant(2*WORDSIZE)
  • 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. El RA y el Heap se ven así: Stack Heap FP i C[0] C C[1] C[2] Saved Registers C[0] [0] C[0] [1] SP C[1] [0] C[1] [1] C[2] [0] C[2] [1]
  • 161. La variable C sería representada así: Memory Operator(-) Register(FP) Constant(WORDSIZE)
  • 162. La variable C[2] así: Memory Operator(-) Memory Operator(*) Operator(-) Constant(WORDSIZE) Constant(2) Register(FP) Constant(WORDSIZE)
  • 163. La variable c[2] [1] Memory Operator(-) Memory Operator(*) Operator(-) Constant(WORDSIZE) Constant(1) Memory Operator(*) Operator(-) Constant(WORDSIZE) Constant(2) Register(FP) Constant(WORDSIZE)
  • 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. ¿Cuál es el árbol de ensamblador para s.y ? Él árbol de s: Memory Register(FP) Para obtener y de s: Memory Operator(-) Memory Constant(WORDSIZE) Register(FP)
  • 166. Memory El árbol de la variable s.x: Memory Register(FP) El árbol para .A[3]: Memory Operator(-) Memory Operator(*) Operator(-) Constant(WORDS Constant(3) IZE) Memory Constant(2*WORD SIZE) Register(FP)
  • 167. Estatutos •Estatutos de Asignación <variable> = <expresión> es representada por el árbol de ensamblador: Move <variable> <expression>