• No se han encontrado resultados

• CUP también asigna una precedencia a cada una de las producciones de la gramática. Esta

N/A
N/A
Protected

Academic year: 2021

Share "• CUP también asigna una precedencia a cada una de las producciones de la gramática. Esta "

Copied!
78
0
0

Texto completo

(1)

Compiladores e Interpretes Compiladores e Interpretes

CUP

LALR Parser Generator for Java Parte 2

Luis Ochoa

ziul1979@gmail.com

(2)

Precedencia de producciones

• CUP también asigna una precedencia a cada una de las producciones de la gramática. Esta

precedencia es igual a la precedencia del último terminal de la producción y, si la producción no precedencia es igual a la precedencia del último terminal de la producción y, si la producción no contiene terminales, se le asigna la menor.

Por ejemplo, la producción expr ::= expr

SUMA expr tendrá la misma precedencia que el terminal SUMA, mientras que expr ::= expr

opBin expr tendrá la menor precedencia entre

las producciones definidas.

(3)

Precedencia de producciones

• Para resolver los conflictos de desplazamiento- reducción, el analizador procede como sigue:

▫ Se determina qué tiene mayor precedencia:

▫ Se determina qué tiene mayor precedencia:

terminal a desplazar o la producción a reducir.

▫ Si el terminal tiene la precedencia mayor, se desplaza.

▫ Si es la producción la que tiene mayor precedencia, se reduce.

▫ Si los dos tienen la misma precedencia, la acción se determina a partir de la asociatividad del

terminal.

(4)

Recuperación de errores

• Cuando el analizador sintáctico generado por CUP recibe una secuencia de testigos que no corresponde a la gramática que ha de analizar, se genera un error y el analizador finaliza su ejecución.

y el analizador finaliza su ejecución.

• CUP proporciona un mecanismo de recuperación ante los errores, consistente en definir un no

terminal especial (error) que encaja toda entrada errónea.

• El símbolo de error sólo está activo en caso de detectarse un error. En este caso, el analizador

intenta reemplazar una secuencia de testigos para

este no terminal y continuar el análisis.

(5)

Recuperación de errores

• El analizador únicamente considerará que la recuperación ha tenido éxito si, después del no terminal error, puede analizar correctamente un terminal error, puede analizar correctamente un determinado número de testigos.

• Veamos un ejemplo a continuación:

(6)

Ejemplo

• Para comprobar la recuperación de errores, modificamos la gramática definida por la calculadora, añadiendo una producción de error:

ecuacion ::= expresion {: System.out.println("Sintaxis correcta");

:} RESULTADO | error {: System.out.println("Sintaxis incorrecta");:} RESULTADO ;

• Esto significa que si se detecta un error en el análisis de una expresión, la recuperación se intentará descartando testigos de la entrada hasta encontrar el testigo

RESULTADO (‘=’).

• Si ejecutamos el analizador, obtenemos:

(7)

Ejemplo

Hay que fijarse en la secuencia en que aparecen los mensajes, para cada entrada:

• Para la entrada (2++2=) se emite un mensaje (Syntax error), generado internamente por el analizador. Aunque la expresión finaliza con un signo igual (‘=’), el analizador aún no ha validado la recuperación delante del error, motivo por el cual no imprime el mensaje contenido en la producción.

• Es después de la entrada (2*1=) cuando el

analizador ha conseguido encajar tres (de hecho,

4) testigos después del error, el momento en que

emite el mensaje “sintaxis incorrecta”, seguido

del mensaje de validación correspondiente a la

tercera expresión.

(8)

Ejemplo

La próxima secuencia de entrada atrasa todavía más la emisión del mensaje:

• Con la expresión errónea (2++3=) emite el

mensaje de error predefinido, pero no imprime el mensaje incluido en la producción, ya que no ha validado la recuperación.

• Con la entrada (2=) aún no ha conseguido encajar los tres testigos que validan la recuperación, y por tanto, no emite todavía ningún mensaje.

• Finalmente, con la última expresión (2+1=) se consiguen tres testigos después de la producción de error, y es aquí cuando se emiten los tres

mensajes correspondientes (uno de error y dos de

validación).

(9)

Recuperación de errores

• A la hora de generar el analizador, el comportamiento de CUP ante una producción de error no difiere del resto de producciones: generará las diferentes tablas como si se tratara de una producción convencional. La diferencia radica en el comportamiento del analizador generado al tratara de una producción convencional. La diferencia radica en el comportamiento del analizador generado al detectar una entrada errónea:

▫ Al encontrar un error, el analizador extraerá símbolos de la pila hasta que encuentre el estado más alto que contenga, dentro de su conjunto de elementos, uno de la forma A ->

error α, donde A es un no terminal y α es una cadena de símbolos o la cadena vacía.

▫ Entonces, el analizador introducirá un componente ficticio

error en la pila, simulando que corresponde a la entrada

actual.

(10)

Recuperación de errores

▫ Si α es la cadena vacía, se produce una reducción a A y se ejecuta la acción asociada a la producción A -> error que, presumiblemente, será una rutina de

recuperación del error que hemos implementado.

Entonces elimina símbolos de entrada hasta que recuperación del error que hemos implementado.

Entonces elimina símbolos de entrada hasta que encuentra un símbolo con el que pueda seguir el análisis normal.

▫ Si α no es vacía, el analizador buscará en la entrada una cadena que se pueda reducir a α. Así, si α sólo contiene terminales, buscará en esta cadena de

terminales en la entrada y los reducirá, desplazándolos a la pila. De esta manera, la cima de la pila contiene

error α y se podrá reducir a A (A -> error α).

(11)

Recuperación de errores

• Después de este proceso seguirá el análisis, sin ejecutar ninguna acción, hasta conseguir analizar correctamente un número error_sync_size() testigos de entrada. Si lo consigue, se considera que ha conseguido recuperarse del error; se vuelve al punto anterior y continúa el

consigue, se considera que ha conseguido recuperarse del error; se vuelve al punto anterior y continúa el

análisis, ejecutando las acciones asociadas que en el

proceso de búsqueda no había ejecutado. Si no consigue analizar este número de testigos, se descarta uno de los testigos de entre los analizados y reinicia la búsqueda más adelante.

• La recuperación de error falla si se llega al final del

archivo de entrada o si, inicialmente, no se encuentra

ninguna producción de error en la pila de estado.

(12)

Ejemplo

• La siguiente representación esquemática, aunque no se corresponda con el

comportamiento real de un analizador LALR, comportamiento real de un analizador LALR, nos permitirá haceros una idea del tratamiento de errores.

• Consideremos el ejemplo anterior y

representemos en columnas los datos relevantes:

(13)

Ejemplo

(14)

Recuperación de errores

• El último aspecto que hay que considerar es la elección de la regla o reglas que tienen que contener las producciones de error.

• En general, cuanto más “arriba” o más cerca a la regla

correspondiente al símbolo inicial de la gramática se sitúa la correspondiente al símbolo inicial de la gramática se sitúa la producción de error, mayor será el número de testigos que se van a ser descartados por el analizador durante la

recuperación.

• Por otra parte si se sitúan muy “abajo” o lejos de la regla inicial, tendremos menos posibilidades de realizar una

recuperación correcta del error. Otro aspecto a considerar es que su ubicación no genere una cadena de nuevos errores.

• Siendo las reglas típicas donde se suele situar producciones de error los subconjuntos de los no terminales que generan

expresiones, proposiciones, bloques y procedimientos.

(15)

Particularización del analizador:

Métodos

• El código generado por CUP contempla una serie de métodos que se pueden definir o sobrescribir, en los casos en que CUP proporciona uno, para poder particularizar el comportamiento del

en los casos en que CUP proporciona uno, para poder particularizar el comportamiento del

analizador sintáctico generado. Estos métodos son:

▫ public void user_init(): Este método es

llamado por el parser antes de solicitar el primer testigo al explorador. El cuerpo del método

contiene el código definido con la cláusula

%init{...%}init.

(16)

Particularización del analizador:

Métodos

▫ public java_cup.runtime.Scanner

getScanner(): Este método retorna el explorador por defecto.

▫ public java_cup.runtime.Symbol scan():

▫ public java_cup.runtime.Symbol scan():

Este método encapsula el explorador y se llama cada vez que el analizador sintáctico precisa un nuevo terminal. Por defecto, retorna:

getScanner().next_token()

▫ public void

setScanner(java_cup.runtime.Scanner s):

Establece el explorador que utilizará el analizador.

(17)

Particularización del analizador:

Métodos

▫ public void report_error(String message, Object info): Este método debería llamarse siempre que haya que mostrar un mensaje de error. En la implementación que proporciona CUP se imprime por la salida estándar de error (System.err) el primer parámetro, que contiene el texto del mensaje de error y, si el segundo parámetro es una instancia el texto del mensaje de error y, si el segundo parámetro es una instancia de la clase Symbol, imprime información adicional sobre la situación, dentro del archivo de entrada, donde se ha producido el error. La

implementación por defecto es:

Siendo habitual sobrescribir este método.

(18)

Particularización del analizador:

Métodos

▫ public void report_fatal_error(String

message, Object info): Este método debe llamarse cuando se produce un error irrecuperable.

Básicamente realiza tres acciones: llama al método

Básicamente realiza tres acciones: llama al método

done_parsing() para detener el análisis, informa del

error con el método anterior y lanza una excepción.

(19)

Particularización del analizador:

Métodos

▫ public void syntax_error(Symbol cur_token): Este método es invocado por el analizador al detectar un error sintáctico, y previamente al intento de recuperarlo. En la

implementación por defecto proporcionada por CUP únicamente implementación por defecto proporcionada por CUP únicamente se llama al método report_error.

▫ public void unrecovered_syntax_error(Symbol

cur_token): Este método es invocado por el analizador si es

imposible recuperarse de un error sintáctico. La implementación

proporcionada por CUP es:

(20)

Particularización del analizador:

Métodos

▫ protected int error_sync_size(): Este método es llamado por el

analizador para determinar cuantos testigos debe analizar correctamente para considerar que la recuperación ha tenido éxito. Por defecto, retorna el valor constante error_sync_size, que tiene el valor 3. En caso de

modificarse, no se recomienda asignarle un valor por debajo de 2.

modificarse, no se recomienda asignarle un valor por debajo de 2.

▫ debug_message (String m): Además del analizador que se ha

utilizado hasta el momento (parse()), CUP proporciona una versión de depuración. Ésta funciona exactamente igual que la anterior, excepto en que imprime mensajes por la salida de error estándar (System.err)

informando del proceso de análisis. Esta información es realizada por el

método debug_message(String m) :

(21)

Particularización del analizador:

Métodos

Ejemplo debug_message (String m):

(22)

Particularización del analizador:

Opciones en la fase de creación

• El comportamiento del analizador generado por CUP puede

particularizarse con las opciones que se pasen al programa en la fase de creación. Recordemos que el comando de ejecución de CUP es:

java java_cup.Main <opciones> <archivo_espec>

java java_cup.Main <opciones> <archivo_espec>

• Introduciendo un comando de CUP puede obtenerse una relación de

estas opciones:

(23)

Particularización del analizador:

Opciones en la fase de creación

-package nombre: Especifica el nombre del paquete en el cual deben situarse las clases sym y parser

-parser nombre: Define el nombre del archivo que contiene el analizador, alternativo al definido por defecto (parser).

-parser nombre: Define el nombre del archivo que contiene el analizador, alternativo al definido por defecto (parser).

-symbols nombre: Hace que la clase que contiene las constantes de los símbolos se llame nombre, en vez de sym.

-interface: Con esta opción, el código de constantes pasa a ser una interfície en lugar de una clase. Es decir, el código generado en el archivo sym.java sin esta opción es:

public class sym { … y con la opción:

public interface sym { ...

(24)

Particularización del analizador:

Opciones en la fase de creación

-nonterms: La clase sym contiene, por defecto, la definición de las constantes que representan los terminales de la gramática. Si se

introduce esta opción también incluirá los valores de los no terminales:

terminales:

El analizador generado por CUP no necesita estos valores y por eso

no los define. Sin embargo, se puede forzar su declaración a efectos

de depurar el analizador.

(25)

Particularización del analizador:

Opciones en la fase de creación

-expect número: Durante la construcción del analizador, CUP puede detectar conflictos de desplazamiento-reducción o de reducción-reducción, no resueltos con la precedencia y asociatividad definida en la gramática. Para estos casos existe la posibilidad de usar la convención siguiente:

Resolver los conflictos de desplazamiento-reducción, aplicando desplazamiento.

Resolver los conflictos de desplazamiento-reducción, aplicando desplazamiento.

Resolver los conflictos de reducción-reducción, aplicando los criterios de dar mayor prioridad a la producción que primero se ha declarado en la especificación.

Para aplicar esta convención, puede utilizarse esta opción, indicando exactamente el número de conflictos esperados. Por defecto, CUP abortará la creación del analizador al detectar cualquiera de estos conflictos (de forma equivalente, podemos decir que por defecto número = 0).

-compact_red: Con esta opción, CUP aplica una optimización para compactar la tabla de reducciones. Con esto puede reducir el tamaño de la tabla de reducciones

considerablemente, pero, por contrapartida, el comportamiento del analizador frente a un error puede empeorar en cuanto a la recuperación.

(26)

Particularización del analizador:

Opciones en la fase de creación

-nowarn: Si usamos esta opción, se suprimen todos los mensajes de alarma (warning) que pueda producir el

sistema.

-nosummary: CUP emite, al finalizar la creación del analizador, un listado resumen como el siguiente:

Con esta opción, no

se emite este listado.

(27)

Particularización del analizador:

Opciones en la fase de creación

-progress: Con esta opción, el sistema produce una serie de mensajes indicando el progreso en el

proceso de creación del analizador.

-dump: Con estas opciones, el sistema genera un volcado de información sobre la gramática

(dump_grammar), los estados (dump_states), las tablas (dump_tables), o todo lo anterior (dump).

-time: Esta opción hace que se emita la información estadística sobre los tiempos de generación del

analizador.

(28)

Particularización del analizador:

Opciones en la fase de creación

-nopositions: Con esta opción CUP no incluye el código de propagación de valores laterales (e1left,

e1right) de terminales a no terminales y de no terminales a otros terminales, ni de las variables correspondientes a las etiquetas a derecha o izquierda. En caso que no sean a otros terminales, ni de las variables correspondientes a las etiquetas a derecha o izquierda. En caso que no sean necesarios estos valores, esto ahorra tiempo en la

ejecución del analizador generado. Si, habiendo generado el analizador con esta opción, se intenta acceder a las variables correspondientes a estas etiquetas, se genera un error.

-version: El sistema imprime información sobre la

versión de CUP que se está ejecutando, sin ninguna

acción más.

(29)

Ejemplo Calculadora

(30)

Ejemplo Calculadora

(31)

Ejemplo Expresiones

• Utilizando el explorador definido por la calculadora anterior, se pretende crear un programa que verifique expresiones de comparación del tipo:

Expresión1 = = Expresión2

• La solución es similar a la anterior, usando la gramática que se reproduce a continuación:

(32)

Ejemplo Calculadora con notación polaca inversa

• A diferencia de la notación convencional, en que el operador se escribe entre dos operandos, la notación inversa escribe el operador después de los operados. Como ejemplo:

• Con esta notación, tal y como muestran los ejemplos de la segunda y tercera fila, la precedencia de los operadores viene definida por el propio orden en que se encuentran en la

entrada y, por tanto, es innecesario el uso de paréntesis.

(33)

Ejemplo Calculadora con notación

polaca inversa

(34)

Interconexión JLex - CUP

• Ahora veremos cómo configurar JLex para que el explorador que genera sea compatible con el analizador generado por CUP.

analizador generado por CUP.

(35)

Estructura del símbolo

• El analizador sintáctico representa a los terminales y no terminales con un objeto de la clase Symbol. Es decir, es el tipo que debe tener el testigo que el explorador entregará al analizador sintáctico y que éste utilizará durante el análisis.

• Esta clase contiene los datos:

La definición de la clase Symbol se encuentra en

• Esta clase contiene los datos:

▫ public Object value: es el valor léxico del terminal o no terminal.

▫ public int left: posición izquierda, dentro del archivo.

▫ public int right: posición derecha, dentro del archivo.

▫ public int sym; : Este valor se corresponde con el valor entero correspondiente al terminal o no terminal, tal y como se ha enumerado en el archivo sym.class.

▫ public int parse_state: estado del análisis. Este campo es utilizado internamente por el analizador sintáctico y no es recomendable su modificación.

Symbol se encuentra en el archivo Symbol.java.

(36)

Estructura del símbolo

• En la sección de la lista de símbolos se enumeraban todos los terminales y no terminales que necesitaba la gramática:

terminal [clase] nombre1, nombre2, ...;

non terminal [clase] nombre1, nombre2, ...;

non terminal [clase] nombre1, nombre2, ...;

• Con la definición de la clase definimos el tipo de datos que contendrá el símbolo durante el análisis sintáctico y deberá contener todos los atributos que sean de interés para la fase de análisis.

• Para los terminales que retornará el explorador, la clase será el tipo de objeto que se especifique en el campo value. Si no se define

ninguna clase para un terminal o no terminal, el objeto será nulo.

(37)

Estructura del símbolo

• La clase Symbol cuenta con constructores con los parámetros siguientes:

• Por tanto, el dato mínimo que espera recibir del

analizador sintáctico del explorador es un valor entero correspondiente a la lista contenida en la clase

Sym.class, tal y como hemos estado haciendo en el

apartado anterior.

(38)

Estructura del símbolo

Si deseamos utilizar todos los datos del objeto Symbol, las reglas contenidas en el archivo de especificación JLex, tendrán un aspecto como sigue:

Donde:

cnt_terminal representa la constante correspondiente al terminal identificado por el explorador, tal y como se ha definido en el archivo Sym.class, y que corresponde al terminal enumerado en la lista de terminales del archivo CUP.

pos_izquierda es el valor entero left del objeto Symbol. En el entorno JLex, este valor puede obtenerse utilizando el valor léxico yychar.

pos_derecha es el valor right del objeto Symbol. En el entorno JLex puede obtenerse como:

yychar + yytext().lenght()

objeto es el parámetro value, de la clase Object, que contiene los datos que, a vuestro criterio, necesitará el analizador sintáctico y que se construye a partir de la lista de valores lista_campos.

(39)

Estructura del símbolo Ejemplo

• A continuación crearemos la gramática JLex

correspondiente a la calculadora desarrollada en el anterior apartado, con las ampliaciones

siguientes:

el anterior apartado, con las ampliaciones siguientes:

▫ Queremos que opere con cualquier entero, no restringido a un solo dígito como hacíamos en el apartado anterior.

▫ El programa creado debe mostrar el resultado de la operación.

Una posible implementación sería:

(40)

Estructura del símbolo Ejemplo

La terminología puede llegar a ser confusa, ya que usamos indistintamente el término

“símbolo” para referirnos a los terminales o no terminales, al objeto que

manipula el analizador sintáctico, al objeto que retorna el explorador y, aún

Se observa que hemos cambiado el nombre del terminal DIGITO por ENTERO, y que el Symbol que éste retorna al analizador sintáctico contiene esta constante (sym.ENTERO) y el objeto (value) consistente en el valor entero del número introducido en la entrada (new Integer(yytext())).

Este segundo valor será utilizado por el analizador sintáctico para calcular el resultado del cálculo, como veremos más adelante.

Ejercicio: Modifica las reglas anteriores para que se controlen los errores de

overflow en la entrada de enteros. Es decir, comprueba que el entero

proporcionado en la entrada está comprendido entre MAX_VALUE y MIN_VALUE.

retorna el explorador y, aún más, a los elementos de la

tabla de símbolos del compilador.

(41)

Compatibilidad JLex-CUP

• CUP precisa que el explorador que le proporciona los

testigos se implemente conforme a la interfície siguiente:

• Para cumplir este requisito, el archivo de especificación

léxica JLex tiene que contener el código siguiente:

(42)

Compatibilidad JLex-CUP

Es decir:

• La primera directiva hace que la clase generada por JLex implemente el explorador exigido por CUP (“Scanner”):

class Yylex implements java_cup.runtime.Scanner { ...

class Yylex implements java_cup.runtime.Scanner { ...

• La segunda hace que la función de análisis léxico que, por defecto, se llama yylex(), pase a llamarse

next_token.

• La tercera directiva especifica que el testigo retornado

por la función de exploración sea del tipo Symbol.

(43)

Compatibilidad JLex-CUP

• Opcionalmente, estas tres opciones se establecen de una forma más compacta con la directiva única:

Ejemplo: Completaremos el analizador léxico JLex para la calculadora que hemos ido desarrollando en los apartados anteriores, de forma que sea

compatible con CUP.

Si, utilizando la directiva

%class..., se modifica el nombre de la clase generada por JLex, habrá que tenerlo en

cuenta al inicializar el parser.

compatible con CUP.

Observa que, con la directiva %cup, el nombre de la clase generada por Jlex mantiene su valor predeterminado (Yylex). Por tanto, si trabajamos con Jlex, la llamada al explorador contenida en el archivo de especificación sintáctica CUP será:

donde flujo_entrada es un objeto de clase java.io.InputStream o java. io.Reader, tal como exige JLex.

(44)

Compatibilidad JLex-CUP

• Una alternativa a la llamada anterior es utilizar los métodos setScanner() y getScanner() que proporciona CUP: •El método parse() es el que realiza el

proporciona CUP: •El método parse() es el que realiza el análisis sintáctico propiamente dicho.

Primeramente parse() inicializa el objeto CUP$action a partir de las tablas del analizador, ejecutando init_actions().

•Entonces ejecuta user_init() para realizar las acciones de inicialización de usuario y hace una búsqueda hacia delante del primer testigo con una llamada a scan().

Finalmente, inicia el análisis sintáctico.

(45)

Compatibilidad JLex-CUP

El análisis continúa hasta que se llama a done_parsing() y se retorna el símbolo, de tipo Symbol, correspondiente a la producción inicial o null si éste no tiene ningún valor asociado.

La función scan() es implementada por CUP como sigue:

Así, si se intenta utilizar el método scan() proporcionado por CUP sin

inicializarlo con la llamada a setScanner(), el programa lanza una excepción Null-PointerException.

Finalmente, debido a que el analizador usa el siguiente testigo de la entrada

durante el análisis (lookahead), no es recomendable modificar el explorador

utilizado por el analizador, mediante setScanner(), cuando ya ha empezado el

análisis.

(46)

Acceso a los valores semánticos

• Tal y como se ha explicado, el analizador representa a los terminales y no terminales con un objeto de la clase

Symbol que contiene los atributos del símbolo en el campo value.

campo value.

• Para acceder a estos atributos, los símbolos contenidos en las reglas sintácticas pueden referenciarse con una etiqueta:

• Entonces, las etiquetas e

i

representan instancias del objeto (value) que contiene el símbolo y desde las

acciones se puede acceder a los diferentes campos con la

referencia convencional de Java:

(47)

Acceso a los valores semánticos

• El ámbito de las etiquetas se restringe a la producción que las

contiene y, por tanto, los nombres se pueden repetir en diferentes producciones y estos valores sólo serán accesibles por las acciones del interior de la producción. Más concretamente, sólo pueden ser del interior de la producción. Más concretamente, sólo pueden ser accedidos por las acciones que queden a la derecha del terminal o no terminal etiquetado.

• Habitualmente, las acciones incluidas en las reglas sintácticas

realizarán operaciones con los atributos del símbolo de la parte

derecha de la producción para asignar valores a los atributos del

terminal de la parte izquierda. Así, al finalizar el análisis, el símbolo

inicial acabará teniendo los atributos que se pretendan obtener con

el análisis sintáctico.

(48)

Acceso a los valores semánticos

De esta forma, el método parse() retorna un

objeto de tipo java_cup.runtime.Symbol que, si el análisis es correcto, contiene el símbolo inicial el análisis es correcto, contiene el símbolo inicial de la gramática, correspondiente al resultado de la última reducción.

• El no terminal de la parte izquierda de la

producción tiene una etiqueta predefinida,

llamada RESULT.

(49)

Acceso a los valores semánticos

Ejemplo: Modificaremos las producciones de la calculadora para que calcule el resultado de las ecuaciones.

Lo que hemos hecho es lo siguiente:

• El terminal ENTERO y el no terminal expresión se han definido con tipo java.lang.Integer.

• Para cada una de las expresiones (suma, resta..) se ha añadido código para definir el valor (value) de la parte izquierda (RESULT) correspondiente a la operación que contiene la expresión. Para hacerlo, se ha creado un nuevo entero (new Integer ...) en que el parámetro del constructor es un entero con el resultado de la operación. Los argumentos se han obtenido con el método intValue().

(50)

Acceso a los valores semánticos

• El resultado conseguido es:

• Además de las variables asociadas a las etiquetas de los símbolos, CUP proporciona dos etiquetas con los nombres nombre_etiquetaright y nombre_etiquetaleft que contienen los valores enteros (int) left (posición izquierda del lexema en el archivo de entrada) y right (posición derecha del lexema dentro del archivo de entrada) del símbolo correspondiente al terminal o no terminal.

• Estos valores deben ser definidos inicialmente por el explorado en los símbolos terminales y el analizador sintáctico generado por CUP automáticamente propaga estos valores hacia los no terminales cuando se reducen las producciones.

(51)

Acceso a los valores semánticos

• La tabla siguiente muestra, en la primera columna, los datos almacenados en el símbolo generado por el explorador y, en la segunda columna, cómo son accedidos por el analizador desde las acciones, considerando que el terminal se ha etiquetado como “e1”.

acciones, considerando que el terminal se ha etiquetado como “e1”.

(52)

Definiciones con atributos por la izquierda

• En el modelo de análisis y síntesis de un compilador, se establecen dos etapas:

▫ Etapa inicial, en que un programa fuente se traduce a

▫ Etapa inicial, en que un programa fuente se traduce a una representación intermedia.

▫ Etapa final, en la que se genera el código objeto.

• Conceptualmente, en la etapa inicial se analiza

sintácticamente la cadena de componentes léxicos de entrada, se construye el árbol de análisis

sintáctico y después se recorre el árbol para evaluar

las reglas sintácticas de sus nodos.

(53)

Definiciones con atributos por la izquierda

• Una posibilidad alternativa es implantarlo en una sola pasada, evaluando las reglas semánticas durante el análisis sintáctico, sin construir

explícitamente un árbol de análisis sintáctico o un grafo que muestre las dependencias entre los atributos. Una aplicación no necesita construir explícitamente un árbol de análisis sintáctico o un grafo de dependencias:

explícitamente un árbol de análisis sintáctico o un grafo de dependencias:

únicamente necesita producir el mismo resultado para cada cadena de entrada.

• Una definición dirigida por la sintaxis es una generalización de una

gramática incontextual en la que cada símbolo gramatical tiene un conjunto de atributos asociados, dividido en dos subconjuntos denominados

atributos sintetizados y atributos heredados del símbolo gramatical.

• De hecho, las reglas semánticas establecen las dependencias entre los atributos que se representarían mediante un grafo. Del grafo de

dependencias se obtiene un orden de evaluación de las reglas semánticas y

la evaluación de las reglas semánticas define el valor de los atributos en los

nodos del árbol de análisis sintáctico para la cadena de entrada.

(54)

Definiciones con atributos por la izquierda

• Una clase importante de éstas se llama

“definiciones con atributos por la izquierda”

y incluye prácticamente todas las traducciones que pueden realizarse sin la construcción explícita de un pueden realizarse sin la construcción explícita de un árbol de análisis sintáctico.

• Una definición dirigida por la sintaxis es una definición con atributos por la izquierda si cada

atributo heredado de Xj , 1 ≤ j ≤ n, del lado derecho de A-> X1 X2 ...Xn depende sólo de:

1. los atributos de los símbolos X1 X2 ...Xj-1 a la izquierda de Xj en la producción, y

2. los atributos heredados de A.

(55)

Definiciones con atributos por la izquierda

• Las “definiciones con atributos sintetizados” son aquéllas que sólo

contienen atributos sintetizados. Toda definición con atributos sintetizados es una definición con atributos por la izquierda.

• En un analizador sintáctico ascendente se pueden evaluar los atributos sintetizados a medida que la entrada es analizada. Es decir, siempre se

• En un analizador sintáctico ascendente se pueden evaluar los atributos sintetizados a medida que la entrada es analizada. Es decir, siempre se puede anotar un árbol de análisis sintáctico para una definición con

atributos sintetizados mediante la evaluación de las reglas semánticas de los atributos en cada nodo de forma ascendente: de las hojas a la raíz.

• Esto es así porque el analizador sintáctico puede conservar en la pila los valores de los atributos sintetizados asociados con los símbolos

gramaticales. Al hacer una reducción se calculan los valores de los nuevos

atributos sintetizados a partir de los atributos que aparecen en la pila para

los símbolos gramaticales del lado derecho de la producción con la que se

reduce.

(56)

Definiciones con atributos por la izquierda

• Cuando se tienen atributos sintetizados y heredados, hay que ir con más cuidado:

▫ Un atributo heredado para un símbolo del lado derecho de una producción debe calcularse en una acción antes de ese una producción debe calcularse en una acción antes de ese símbolo.

▫ Una acción no debe referirse a un atributo sintetizado de un símbolo que esté a la derecha de la acción.

▫ Un atributo sintetizado por el no terminal de la izquierda

sólo se puede calcular después de que se hayan calculado

todos los atributos a los que hace referencia.

(57)

Definiciones con atributos por la izquierda

• Siempre es posible empezar

con una definición dirigida

por la sintaxis con atributos

por la izquierda y construir un

esquema que cumpla los tres

esquema que cumpla los tres

requisitos anteriores. Por

ejemplo:

(58)

Definiciones con atributos por la

izquierda

(59)

La tabla de símbolos

La tabla de símbolos es la estructura de datos utilizada por el compilador para gestionar los identificadores que aparecen en el programa fuente incluyendo, entre otros, las variables, tipos y acciones.

Esta tabla es utilizada en varias fases del proceso de compilación:

El analizador léxico verifica si el identificador está introducido en la tabla de símbolos y, si no

El analizador léxico verifica si el identificador está introducido en la tabla de símbolos y, si no lo está, crea una nueva entrada para él.

El analizador sintáctico normalmente añade información en los campos, aunque también puede crear nuevas entradas.

El analizador semántico debe acceder a la tabla de símbolos para consultar el tipo de datos de los símbolos.

El generador de código puede acceder a la tabla para obtener información (asignación de memoria a las variables, por ejemplo) y para añadirla (asignación de direcciones de memoria, por ejemplo).

Ahora veremos la implementación y explotación de una tabla de símbolos con

una tabla de dispersión y, concretamente, utilizando la clase hashtable de Java.

(60)

Tabla de dispersión (hashtable)

• Las tablas de dispersión son estructuras de datos lineales,

habitualmente implementadas con una matriz unidimensional, en que cada elemento está constituido por una clave y un valor

asociado. La posición que corresponde a un determinado elemento asociado. La posición que corresponde a un determinado elemento se obtiene aplicando una función de dispersión a la clave.

• Cuando varias claves, al aplicar la función de dispersión, dan un

mismo resultado se dice que se ha producido una colisión. La tabla

puede utilizar una lista encadenada en cada elemento, para resolver

colisiones. En este caso se llama tabla de dispersión abierta. La lista

encadenada permite almacenar varios elementos, correspondientes

a aquéllos para los cuales la función de dispersión retorna el mismo

resultado.

(61)

Tabla de dispersión (hashtable)

• El número de celdas de la tabla se denomina capacidad y el factor de carga es la relación entre el número de celdas ocupadas y el tamaño de la tabla.

• Java dispone de la clase hashtable que implementa una tabla de dispersión abierta. Esta clase hereda de

Dictionary, que a su vez lo hace de Object, y pertenece al paquete java.util.

• Tanto para la clave como para el valor puede utilizarse

cualquier objeto no nulo. El objeto que se utilice como

clave debe tener implementado los métodos hashCode y

equals

(62)

Tabla de dispersión (hashtable)

• Cuando el número de elementos utilizados en la tabla supera el producto del factor de carga por la capacidad de la tabla, la capacidad de la tabla se incrementa automáticamente. Esta función de redimensionamiento se realiza con el método rehash.

• El valor del factor de carga asignado por defecto es 0.75. Valores más altos incrementan el tiempo de búsqueda de una entrada. Cabe destacar los

constructores siguientes:

▫ Hashtable(): Construye una nueva tabla de dispersión, vacía, con la capacidad y factor de carga por defecto.

▫ Hashtable(int capacidadInicial): Construye una nueva tabla de dispersión, vacía, con la capacidad especificada y factor de carga por defecto.

▫ Hashtable(int capacidadInicial, float factorCarga): Construye una nueva tabla de dispersión, vacía, con la capacidad y factor de carga especificados.

(63)

Tabla de dispersión (hashtable)

• Y los métodos:

(64)

Ejemplo Ampliado

• Se pretende crear, utilizando los ejemplos anteriores, una calculadora simbólica. Ésta leerá un archivo de texto estructurado en dos secciones:

Sección de declaraciones.

▫ El archivo empieza con las declaraciones de las variables que se utilizarán.

▫ Es obligatorio declarar todas la variables utilizadas.

▫ El cuerpo de declaraciones se delimita con las palabras reservadas VAR y FINVAR.

▫ Para declarar las variables se utiliza la sintaxis siguiente:

nombrevar1 [, nombrevar2 [, ...]] : tipo;

donde tipo puede ser ENTERO o FLOTANTE, que representan a los tipos

entero (int) y real (float).

(65)

Ejemplo Ampliado

• Sección de código.

▫ El código permitido contiene sentencias de asignación del tipo:

asignación del tipo:

nombrevar = expresion;

▫ Las expresiones permitidas son las descritas en el ejemplo de la calculadora desarrollada en este

manual.

(66)

Ejemplo Ampliado

Además:

▫ El archivo empieza y acaba con las palabras reservadas PROGRAMA y FIN.

▫ El programa mostrará el valor final de todas las variables declaradas.

▫ Si algun operando es FLOTANTE y el resultado es ENTERO, se generará un error.

▫ Si se utilizan variables no inicializadas se generará un error.

▫ El lenguaje no es sensible a mayúsculas.

▫ El lenguaje no es sensible a mayúsculas.

▫ Se permite el uso de comentarios tipo lenguaje C (/* comentario */).

(67)

Ejemplo Ampliado

(68)

Ejemplo Ampliado

El diagrama de una expresión es similar al El diagrama de una expresión es similar al explicado anteriormente, excepto en que hay dos terminales más (los identificadores y los valores de tipo FLOTANTE) que también son expresiones posibles.

(69)

Ejemplo Ampliado

• Esta gramática lo podemos ver acá:

• Donde Las producciones de error de la

gramática las situaremos en el interior de las gramática las situaremos en el interior de las ecuaciones y declaraciones, utilizando el

punto y coma (‘;’) como terminal de recuperación.

• Los datos que necesariamente debe proporcionar el analizador léxico son:

▫ Para todas las palabras reservadas, el valor

sym.terminal correspondiente resulta suficiente.

▫ Para las constantes enteras o flotantes, se

necesita que disponer de su valor, en el formato de tipo correspondiente.

(70)

Ejemplo Ampliado

▫ El objeto (Symbol) que devolverá el explorador para los identificadores deberá ajustarse a las necesidades del análisis sintáctico. Consideramos que el objeto value

devuelto por el explorador y gestionado por el analizador sintáctico contiene la información siguiente:

sintáctico contiene la información siguiente:

 Lexema correspondiente, para poder acceder a la tabla de símbolos.

 Tipo de datos que contiene (entero, flotante o no definido).

 Los valores para estos posibles tipos de datos.

Esta estructura se implementará con la clase testigo.

(71)

Ejemplo Ampliado

• Así, la lista de terminales y no terminales del archivo de

especificación será:

• Observemos en que la clase testigo se asigna también al no terminal expresion, y los no terminales lista y tipo se

declaran de tipo Integer. Esto se hace de esta forma porque los atributos que se

transferirán por el árbol

sintáctico precisan que estos

no terminales tengan una clase

que les pueda contener.

(72)

Ejemplo Ampliado

• El atributo “tipo” que puede tener como valores ENTERO o

FLOTANTE, y que representaremos con un Integer, nos llegará a la fase de análisis con el terminal TENTERO o TFLOTANTE. A partir de la

gramática:

• Se puede ver que el atributo se transferirá al no terminal tipo (expresiones 4 y 5). De éste a lista, por la tercera expresión y,

finalmente, al identificador (ID), por la primera y segunda expresiones.

Así, tanto tipo, como lista, tiene que ser Integer para realizar esta

función. Podemos aplicar un razonamiento similar para la clase testigo

con el no terminal expresion.

(73)

Ejemplo Ampliado

• Siguiendo con los identificadores, será necesario crear y gestionar una tabla de símbolos para poder detectar identificadores

duplicados o no declarados.

• El analizador léxico los introducirá en la tabla de símbolos y, con el

• El analizador léxico los introducirá en la tabla de símbolos y, con el análisis descrito en el párrafo anterior, será el analizador sintáctico el que les asignará el tipo de datos que contiene. Igualmente, si

alguno de los identificadores cargados en la tabla de símbolos por el analizador léxico no ha sido declarado, será el analizador el que lo identificará o tratará (eliminándolos, por ejemplo).

• La tabla de símbolos contendrá, básicamente, la misma información

que los testigos, además de una serie de marcadores para indicar si

la variable se ha inicializado, declarado o usado.

(74)

Ejemplo Ampliado

• Dentro del analizador sintáctico, realizaremos una pequeña comprobación de tipos. Si, por ejemplo, tenemos una sentencia:

ejemplo, tenemos una sentencia:

Var = 3.25;

• donde Var está declarada como una variable

entera, estaremos ante un error semántico. No

es un error sintáctico, ya que la estructura de la

sentencia es correcta, y no afectará al resto del

análisis sintáctico, pero será un error a tratar.

(75)

Ejemplo Ampliado

• Igualmente, si tenemos var1=var2+var3; y var2 es flotante, el resultado será flotante. Hará falta comprobar que var1 sea de este tipo.

comprobar que var1 sea de este tipo.

• A continuación se incluye el código del archivo CUP para esta gramática. Hacemos unas cuantas aclaraciones al respecto:

▫ La tabla de símbolos, variables y funciones

utilizadas por las acciones sintácticas se incluyen

en el interior de action code{: ...:}.

(76)

Ejemplo Ampliado

▫ La gestión del proceso se realiza con la función main(...) incluida en parser code{: .. :}. Se permite la entrada por teclado o desde un archivo, y puede funcionar en modo depuración, caso en el que emite

información del proceso de análisis.

▫ Esta funcionalidad puede dificultar la lectura del código pero, durante la

▫ Esta funcionalidad puede dificultar la lectura del código pero, durante la ejecución, facilita la comprensión del proceso de análisis de un LALR.

▫ Las acciones sintácticas básicamente transfieren valores de la parte

derecha hacia la parte izquierda de la regla, o a otros símbolos de la parte derecha. Por ejemplo, con la regla:

expresion ::= expresion:e1 SUMA expresion:e2

▫ Lo que hace es asignar a la parte izquierda (RESULT) el valor de la suma de los dos operandos:

RESULT = e1.valor + e2.valor

(77)

Ejemplo Ampliado

• Hay que considerar que, además, se realiza:

▫ Una verificación de tipo y una conversión al tipo resultante, si procede.

▫ La comprobación de que los operandos no están indefinidos; es decir, si no se les ha asignado ningún valor entero o float y, por tanto, el resultado de la operación queda indefinida.

• Si no se han producido errores, se muestra el resultado de la ejecución del

programa:

(78)

Ejemplo Ampliado

• Ver solución y probar la solución en el archivo:

Extracto Manual CUP Código Extracto Manual CUP Código

Calculadora.pdf

Referencias

Documento similar

BUBER'NEUaiAMN, Margarete ¡ Von Potsáam ndch Moskau. SMíionen eines Irftveges. Stuttgart, 1957; Deutsche Verlags-Anstalt, 480 págs... DAHM: Deutsches Reckt. Die geschichüichen

Argumentación y coaching del tutor por equipos Ver y escuchar rutinas semanales por expertos de casos reales. Integración en equipos en agencia

Pero cuando vio a Mar sacar el fuego de bajo su ala, voló de vuelta a su tribu a contarles lo que había visto.... Justo antes de que el sol saliera, Tatkanna se despertó y comenzó

o esperar la resolución expresa&#34; (artículo 94 de la Ley de procedimiento administrativo). Luego si opta por esperar la resolución expresa, todo queda supeditado a que se

Gastos derivados de la recaudación de los derechos económicos de la entidad local o de sus organis- mos autónomos cuando aquélla se efectúe por otras enti- dades locales o

1. LAS GARANTÍAS CONSTITUCIONALES.—2. C) La reforma constitucional de 1994. D) Las tres etapas del amparo argentino. F) Las vías previas al amparo. H) La acción es judicial en

¿Cómo se traduce la incorporación de ésta en la idea de museo?; ¿Es útil un museo si no puede concebirse como un proyecto cultural colectivo?; ¿Cómo puede ayudar el procomún

Asimismo una reflexión calmada sobre las intrincadas relaciones existentes en el péndulo que va del ODM 1 al ODM 8, debería conducirnos a observar por qué las formas mediante las