CAPITULO 3 COMP

Anuncio
3.1
Análisis sintáctico
En esta fase los caracteres o componentes léxicos se agrupan jerárquicamente en
frases gramaticales que el compilador utiliza para sintetizar la salida. Se comprueba si
lo obtenido de la fase anterior es sintácticamente correcto (obedece a la gramática del
lenguaje). Por lo general, las frases gramaticales del programa fuente se representan
mediante un árbol de análisis sintáctico.
La estructura jerárquica de un programa normalmente se expresa utilizando reglas
recursivas. Por ejemplo, se pueden dar las siguientes reglas como parte de la
definición de expresiones:
1. Cualquier identificador es una expresión.
2. Cualquier número es una expresión.
3. Si expresión1 y expresión2 son expresiones, entonces también lo son:
 expresión1 + expresión2
 expresión1 * expresión2
 ( expresión1 )
Las reglas 1 y 2 son reglas básicas (no recursivas), en tanto que la regla 3 define
expresiones en función de operadores aplicados a otras expresiones.
La división entre análisis léxico y análisis sintáctico es algo arbitraria. Un factor para
determinar la división es si una construcción del lenguaje fuente es inherentemente
recursiva o no. Las construcciones léxicas no requieren recursión, mientras que las
construcciones sintácticas suelen requerirla. No se requiere recursión para reconocer
los identificadores, que suelen ser cadenas de letras y dígitos que comienzan con una
letra. Normalmente, se reconocen los identificadores por el simple examen del flujo de
entrada, esperando hasta encontrar un carácter que no sea ni letra ni dígito, y
agrupando después todas las letras y dígitos encontrados hasta ese punto en un
componente léxico llamado identificador. Por otra parte, esta clase de análisis no es
suficientemente poderoso para analizar expresiones o proposiciones. Por ejemplo, no
podemos emparejar de manera apropiada los paréntesis de las expresiones, o las
palabras begin y end en proposiciones sin imponer alguna clase de estructura
jerárquica o de anidamiento a la entrada.
Análisis semántico
La fase de análisis semántico revisa el programa fuente para tratar de encontrar
errores semánticos y reúne la información sobre los tipos para la fase posterior de
generación de código. En ella se utiliza la estructura jerárquica determinada por la fase
de análisis sintáctico para identificador los operadores y operandos de expresiones y
proposiciones.
Un componente importante del análisis semántico es la verificación de tipos. Aquí, el
compilador verifica si cada operador tiene operandos permitidos por la especificación
del lenguaje fuente. Por ejemplo, las definiciones de muchos lenguajes de
programación requieren que el compilador indique un error cada vez que se use un
número real como índice de una matriz. Sin embargo, la especificación del lenguaje
puede permitir coerciones a los operandos, por ejemplo, cuando un operador
aritmético binario se aplica a un número entero y a un número real.
- 19 -
3.1.1
Gramáticas libres de contexto
Una gramática libre de contexto en lingüística e informática es una gramática formal
en la que cada regla de producción es de la forma:
V→w
Donde V es un símbolo no terminal y w es una cadena de terminales y/o no
terminales. El término libre de contexto se refiere al hecho de que el no terminal V
puede siempre ser sustituido por w sin tener en cuenta el contexto en el que
ocurra. Un lenguaje formal es libre de contexto si hay una gramática libre de
contexto que lo genera.
Las gramáticas libres de contexto permiten describir la mayoría de los lenguajes
de programación, de hecho, la síntaxis de la mayoría de lenguajes de
programación está definida mediante gramáticas libres de contexto. Por otro lado,
estas gramáticas son suficientemente simples como para permitir el diseño de
eficientes algoritmos de análisis sintáctico que, para una cadena de caracteres
dada determinen como puede ser generada desde la gramática. Los analizadores
LL y LR tratan restringidos subconjuntos de gramáticas libres de contexto.
La notación más frecuentemente utilizada para expresar gramáticas libres de
contexto es la forma Backus-Naur.
Así como cualquier gramática formal, una gramática libre de contexto puede ser
definida mediante la 4-tupla:
La definicón formal de las gramaticas libres de contexto es la siguiente:
G = (Vt,Vn,P,S) donde
Vt es un conjunto finito de terminales
Vn es un conjunto finito de no terminales
P es un conjunto finito de producciones
el denominado Símbolo Inicial
los elementos de P son de la forma
Ejemplo
Una simple gramática libre de contexto es
S → aSb | ε
donde | es un o lógico y es usado para separar múltiples opciones para el mismo
no terminal, ε indica una cadena vacía. Esta gramática genera el lenguaje no
regular
.
Ejemplo
Aquí hay una gramática libre de contexto para expresiones enteras algebraicas
sintácticamente correctas sobre las variables x, y y z:
S → x | y | z | S + S | S - S | S * S | S/S | (S)
Generaría, por ejemplo, la cadena ( x + y ) * x - z * y / ( x + x )
Ejemplo
- 20 -
Una gramática libre de contexto para un lenguaje consistente en todas las cadenas que
se pueden formar con las letras a y b, habiendo un número diferente de una que de
otra, sería:
S→U|V|0
U → TaU | TaT
V → TbV | TbT
T → aTbT | bTaT | ε
T genera todas las cadenas con la misma cantidad de letras a que
b, U genera todas las cadenas con más letras a, y V todas las
cadenas con más letras b.
3.1.2
Árboles de análisis gramáticas y árboles sintácticos abstractos
Existen básicamente dos formas de describir cómo en una cierta gramática una cadena
puede ser derivada desde el símbolo inicial. La forma más simple es listar las cadenas
de símbolos consecutivas, comenzando por el símbolo inicial y finalizando con la
cadena y las reglas que han sido aplicadas. Si introducimos estrategias como
reemplazar siempre el no terminal de más a la izquierda primero, entonces la lista de
reglas aplicadas es suficiente. A esto se le llama derivación por la izquierda. Por
ejemplo, si tomamos la siguiente gramática:
(1) S → S + S
(2) S → 1
y la cadena "1 + 1 + 1", su derivación a la izquierda está en la lista [ (1),
(1), (2), (2), (2) ]. Análogamente, la derivación por la derecha se define
como la lista que obtenemos si siempre reemplazamos primero el no terminal
de más a la derecha. En ese caso, la lista de reglas aplicadas para la
derivación de la cadena con la gramática anterior sería la [ (1), (2), (1), (2),
(2)].
La distinción entre derivación por la izquierda y por la derecha es importante
porque en la mayoría de analizadores, la transformación de la entrada es
definida dando una parte de código para cada producción que es ejecutada
cuando la regla es aplicada. De modo que es importante saber qué derivación
aplica el analizador, por que determina el orden en el que el código será
ejecutado.
Una derivación también puede ser expresada mediante una estructura
jerárquica sobre la cadena que está siendo derivada. Por ejemplo, la
estructura de la derivación a la izquierda de la cadena "1 + 1 + 1" con la
gramática anterior sería:
S→S+S (1)
S→S+S+S (1)
S→1+S+S (2)
S→1+1+S (2)
S→1+1+1 (2)
{ { { 1 }S + { 1 } S }S + { 1 }S }S
Donde { ... }S indica la sub-cadena reconocida como perteneciente a S. Esta
jerarquía también se puede representar mediante un árbol sintáctico:
- 21 -
Figura 3.1
La derivación por la derecha:
S→
S→
S→
S→
S→
S
1
1
1
1
+
+
+
+
+
S (1)
S (2)
S + S (1)
1 + S (2)
1 + 1 (2)
define el siguiente árbol sintáctico:
Figura 3.2
Propiedades de los lenguajes libres de contexto
Una de las definiciones alternativas y equivalentes de lenguaje libre de contexto
emplea autómatas no deterministas: un lenguaje es libre de contexto si puede ser
aceptado por ese autómata. Un lenguaje puede ser también modelado como un
conjunto de todas las secuencias de terminales aceptadas por la gramática. Este
modelo ayuda a entender las operaciones de conjuntos sobre lenguajes.
La unión y concatenación de dos lenguajes libres de contexto es también libre de
contexto. La intersección no tiene por que serlo. El inverso de un lenguaje libre de
contexto es también libre de contexto, pero el complemento no tiene por que serlo.
Los lenguajes regulares son libres de contexto por que pueden ser descritos
mediante una gramática regular. La intersección de un lenguaje libre de contexto y
un lenguaje regular es siempre libre de contexto.
Existen Gramáticas_sensibles_al_contexto que no son libres de contexto.
- 22 -
Para demostrar que un lenguaje dado no es libre de contexto, se puede emplear el
Lema del bombeo para lenguajes libres de contexto. El problema de determinar si
una gramática sensible al contexto describe un lenguaje libre del contexto es
indecidible.
3.2
Notaciones extendidas: EBNF y diagramas de sintaxis
La notación BNF suele usarse para describir sintácticamente lenguajes de
programación. Se remonta al año 60, en que John Backus la usó para describir el
lenguaje Algol. De hecho, es el acrónimo de Backus Naur Form, en homenaje a la
destacada contribución de Backus (coordinador del grupo que desarrolló el lenguaje
FORTRAN), y de Peter Naur (coordinador del grupo que desarrolló el lenguaje Algol)
al diseño de lenguajes de programación.
La definición formal de la sintaxis de un Lenguaje de Programación se conoce
comúnmente como gramática. Una gramática se compone de un conjunto de reglas
(producciones) que definen las palabras (elementos léxicos) y unidades sintácticas.
Una gramática formal es una que usa una notación definida (o metalenguaje) de
manera estricta. Cuando se considera la estructura de una oración en Español, se le
describe por lo general como una secuencia de categorías:
sujeto | verbo | complemento
Podemos decir que una oración puede ser una oración declarativa o una oración
interrogativa, lo que denotamos:
<oración> ::= <declarativa> | <interrogativa>
donde "::=" significa "se define cómo" y "|" significa "o"; comparándolo con el
lenguaje usado en las gramáticas formales, las palabras encerradas entre <palabra>
son categorías sintácticas o no terminales, es decir deben ser definidas; "::=" equivale
a "->" y "|" tiene el mismo significado. Así, por ejemplo, en gramáticas formales:
O -> D | I
S -> S V C
S -> A N
I -> ¿ V S P ?
en nuestro metalenguaje:
<oración> ::= <declarativa> | <interrogativa>
<declarativa> ::= <sujeto> <verbo> <complemento>
<sujeto> ::= <artículo> <nombre>
:
<interrogativa> ::= ¿ <verbo> <sujeto> <predicado> ?
:
Esta notación específica se conoce como BNF (Backus Naur Form) desarrollada
por John Backus alrededor de 1960 para la definición de ALGOL.
Para expresar repetición se usa la recursividad, por ejemplo:
<entero> ::= <entero> <dígito> | <dígito>
define a un entero como una secuencia de dígitos, con a lo menos un dígito.
A pesar de su estructura sencilla, BNF sirve para definir casi todos los lenguajes
de programación. Las áreas de sintaxis que no pueden definirse con una BNF y
- 23 -
tampoco con una gramática libre del contexto, son aquellas que indican dependencia
contextual. Por ejemplo, "un arreglo declarado con dos dimensiones, no se puede
referenciar con tres subíndices", o "en un comando de atribución, la variable del lado
izquierdo debe ser del mismo tipo de la expresión del lado derecho", etc.
Notación BNF extendida: Las extensiones siguientes permiten realizar descripciones
más fáciles de los lenguajes:
Sintaxis Significado
::=
se define como
t'
el símbolo terminal t
<nt>
el símbolo no terminal nt
(...)
usado para agrupar
*
cero o más repeticiones del elemento anterior
+
una o más repeticiones del elemento anterior
[...]
elemento discrecional
|
alternativa de varias formas sintácticas válidas
Tabla 3.1
Ejemplo:
<entero> ::= [+|-] <dígito> {<dígito>}*
<identificador> ::= <letra> {<letra> | <dígito>}*
Diagramas sintácticos
Los diagramas sintácticos son una forma gráfica de expresar la BNF extendida.
Cada regla está representada por un camino que va desde la entrada ubicada a la
izquierda, hasta la salida, ubicada a la derecha. Cualquier trayecto desde la entrada a
la salida representa un string generado por esa regla. Las categorías sintácticas (noterminales) se representan por rectángulos y los símbolos terminales por círculos u
óvalos. Ejemplo:
Entero
- 24 -
Figura 3.3
Veamos un ejemplo de asignación con los dos metalenguajes:
a. En BNF:
<comando asignación> ::= <variable> := <expresión aritmética>
<expresión aritmética> ::= <término> | <expresión aritmética> + <término> |
<expresión aritmética> - <término>
<término> ::= <primario> | <término> * <primario> | <término> / <primario>
<primario> ::= <variable> | <número> | (<expresión aritmética>)
<variable> ::= <identificador> | <identificador> [<lista subíndice>]
<lista subíndice> ::= <expresión aritmética> | <lista subíndice> , <expresión
aritmética>
b. En BNF extendida:
<comando asignación> ::= <variable> := <expresión aritmética>
<expresión aritmética> ::= <término> { [+|-] <término> }*
<término> ::= <primario> { [*|/] <primario> }
<primario> ::= <variable> | <número> | (<expresión aritmética>)
<variable> ::= <identificador> | <identificador> [<lista subíndice>]
<lista subíndice> ::= <expresión aritmética> { , <expresión aritmética> } *
3.4
Análisis sintáctico descendente (mediante método descendente recursivo)
una gramática libre de contexto G=(Σ,N,P,S) y una frase de entrada w ∈ Σ*, un
Analizador Sintáctico Descendente (Top-Down Parser) intenta encontrar una derivación
izquierda de la frase partiendo del símbolo no-terminal inicial S y reemplazando el noterminal más a la izquierda por el lado derecho de una producción que tenga a ese noterminal por lado izquierdo.
Dada
Configuración
Una configuración es una tupla que describe el estado de una máquina o procedimiento
de análisis sintáctico en un momento dado. En el caso del Analisis Sintactico
Descendente, dada una gramática G=(Σ,N,P,S), una configuración es una tupla:
⟨w$, Xα$, Π⟩
donde:
- 25 -
w ∈ Σ*
Xα ∈ (Σ ∪ N)*, S ⇒* a
Π=p 1 p 2 ... p n | p i∈ P
Y donde w es la parte de la cadena de entrada que queda por consumir, Xa es una
forma sentencial obtenida mediante una derivación izquierda, y X es el símbolo más a
la izquierda en dicha sentencia, y Π es la secuencia de producciones que produjeron
dicha derivación. El símbolo $ es un nuevo símbolo que no está en S ni en N, y que
usamos para denotar tanto el final de la secuencia de entrada como el de la forma
sentencial.
Movimientos o Cómputos
Un movimiento en una derivación es el paso de una configuración a otra, y se indica
por el símbolo ├─. Los símbolos ├─ *, ├─ +, y ├─ k tienen los significados usuales.
Derivaciones y Configuraciones
Con las configuraciones podemos describir de manera conveniente un proceso de
derivación:
1. Si el símbolo más a la izquierda de la forma sentencial es un no-terminal, el mismo se
substituye por la parte derecha de una de las producciones.
2. Si el símbolo más a la izquierda en la forma sentencial es un no-terminal distinto al
primer símbolo en la entrada, se retrocede hasta la última sustitución de un noterminal y se selecciona una producción distinta para hacer la sustitución. Si no hay
más producciones que elegir, se aplica otro retroceso. Si es posible retroceder, la frase
de entrada no pertenece al lenguaje
3. Cada vez que en una configuración el primer símbolo de la entrada coincide con el
primer símbolo en la forma sentencial derivada hasta el momento, ambos símbolos se
consumen.
Ejemplo
Dada la gramática
1.
2.
3.
4.
5.
S→(L)
L→AL
L→A
A→S
A→a
Hecho eso, podemos usar configuraciones para describir el proceso de análisis
descendente de la frase de entrada: ((a)(a)a).
⟨((a)(a)a)$,S$,⟩
sustituir 1
├─⟨((a)(a)a)$,(L)$,1⟩
consumir (
├─⟨(a)(a)a)$,L)$,1⟩
sustituir 2
- 26 -
├─⟨(a)(a)a)$,AL)$,1 2⟩
sustituir 4
├─⟨(a)(a)a)$,SL)$,1 2 4⟩
sustituir 1
├─⟨(a)(a)a)$,(L)L)$,1 2 4 1⟩
consumir (
├─⟨a)(a)a)$,L)L)$,1 2 4 1⟩
sustituir 3
├─⟨a)(a)a)$,A)L)$,1 2 4 1 3⟩
sustituir 5
├─⟨a)(a)a)$,a)L)$,1 2 4 1 3 5⟩
consumir a
├─⟨)(a)a)$,)L)$,1 2 4 1 3 5⟩
consumir (
├─⟨(a)a)$,L)$,1 2 4 1 3 5⟩
sustituir 2
├─⟨(a)a)$,AL)$,1 2 4 1 3 5 2⟩
sustituir 4
├─⟨(a)a)$,SL)$,1 2 4 1 3 5 2 4⟩
sustituir 1
├─⟨(a)a)$,(L)L)$,1 2 4 1 3 5 2 4 1⟩
consumir (
├─⟨a)a)$,L)L)$,1 2 4 1 3 5 2 4 1⟩
sustituir 3
├─⟨a)a)$,A)L)$,1 2 4 1 3 5 2 4 1 3⟩
sustituir 5
├─⟨a)a)$,a)L)$,1 2 4 1 3 5 2 4 1 3 5⟩
consumir a
├─⟨)a)$,)L)$,1 2 4 1 3 5 2 4 1 3 5⟩
consumir )
├─⟨a)$,L)$,1 2 4 1 3 5 2 4 1 3 5⟩
sustituir 3
├─⟨a)$,A)$,1 2 4 1 3 5 2 4 1 3 5 3⟩
sustituir 4
├─⟨a)$,S)$,1 2 4 1 3 5 2 4 1 3 5 3 4⟩
sustituir 1
⟨a)$,(L)$,1 2 4 1 3 5 2 4 1 3 5 3 4 1⟩ retroceder 1
⟨a)$,S$,1 2 4 1 3 5 2 4 1 3 5 3 4⟩
retroceder 4
├─⟨a)$,A)$,1 2 4 1 3 5 2 4 1 3 5 3⟩
sustituir 5
├─⟨a)$,a)$,1 2 4 1 3 5 2 4 1 3 5 3 5⟩
consumir a
├─⟨)$,)$,1 2 4 1 3 5 2 4 1 3 5 3 5⟩
consumir )
├─⟨$,$,1 2 4 1 3 5 2 4 1 3 5 3 5⟩
fin
Como la derivación anterior demuestra, el análisis sintáctico descendente recursivo con
retroceso (ASDRR) puede ser un proceso costoso incluso para gramáticas y frases de
entrada sencillas. En particular, un ASDRR puede realizar una cantidad enorme de
trabajo antes de darse cuenta que la decisión tomada en uno de los pasos iniciales fue
equivocada.
- 27 -
3.4.1
Análisis sintáctico LL(1)
Es del tipo LL1 porque empezamos a derivando por la izquierda y los caracteres son leídos
de izquierda a derecha, el 1 por que se lee 1 solo elemento de entrada. También se puede
considerar como un intento de construir un árbol de análisis sintáctico para la entrada
comenzando desde la raíz y creando los nodos del árbol en orden previa. Bueno
primeramente para trabajar el análisis sintáctico descendente se debe realizar primeramente
algunas operaciones para que la gramática sea LL1 las cuales son:
- Eliminar Ambigüedad - Eliminar Recursividad por la Izquierda – Factorizar - Primeros y
siguientes

Ambigüedad
Una gramática es ambigua cuando genera más de un árbol de derivación.
Para eliminar la ambigüedad se debe reescribir la gramática.
Ejemplo:

Recursividad por la Izquierda
Una gramática es recursiva por la izquierda si tiene un no Terminal A tal que existe una
derivación A->Aα para alguna cadena . Es decir por simple observación podemos
identificar.
Para eliminar la recursividad por la izquierda se utiliza la siguiente formula.
- 28 -
Ejemplo:
Gramática Recursiva
3.4.2
Un analizador sintáctico descendente recursivo para el lenguaje TINY
Como objetivo final, esta práctica propone el desarrollo de un analizador sintáctico
descendente para “miLenguaje”. Como primer paso, es necesario escribir la gramática
del lenguaje que se ha especificado en la primera práctica. Es aconsejable, primero,
definir una gramática “intuitiva” para, en un paso posterior, tratar de transformarla en
una que sea LL(1). Una gramática para un lenguaje procedural tipo Ada puede
empezar como:
programa ->
tkPROCEDURE
tkID
listaParsFormales
tkIS
parteDecs
bloqueInst
’;’
listaParsFormales ->
| listaParsF
listaParsF ->
listaParsF ’;’ decPar
| decPar
...
Es importante que antes de empezar a implementar el analizador meditar
concienzudamente sobre la gramática.
Resultados
Como resultado debemos tener el fichero denominado pract3.tar, de manera que la
ejecución de la instrucción tar xvf pract3.tar genere un directorio denominado pract3
con los siguientes ficheros:
- 29 -
AL_miLenguaje_2.l que contiene el fuente Flex del analizador léxico usado (notar que
el AS_LL1.c que contiene el fuente C con la implementación del analizador sintáctico.
La implementación puede realizarse tanto por tabla como descendente recursivo.
AS_LL1Make que contiene el fichero Make para generar el ejecutable del analizador
sint´actico, de manera que la invocación make -f AS_LL1Make genere el ejecutable
AS_LL1 que realiza el an´alisis sint´actico. La invocación al analizador debe ser como
sigue (el significado del número 3 en la invocación se explicará más tarde):
AS_LL1 3 nombreDelFicheroConElFuente CPS. Universidad de Zaragoza. Compiladores
I. Curso 07-08 3.Todos aquellos ficheros que sean necesarios (conteniendo, por
ejemplo, implementaciones de tipos de datos, funciones para el tratamiento de
errores, etc.).
El analizador léxico no debe dar ningún mensaje (ni realizar ninguna tarea adicional)
cuando el fuente sea léxica y sintácticamente correcto.
En caso de error léxico, el comportamiento ha de ser el establecido en la segunda
práctica. En caso de error sintáctico, se debe suministrar información al usuario como
se muestra en el siguiente ejemplo. Considerar, como ejemplo, el programa en Ada:
Procedure is
a, c: integer;
Begin
get(a);
If a>4 Then
c:=a;
End If;
put("Se acabó"); new_line;
End;
Deberá dar un mensaje de error del estilo del siguiente:
(1) Error:lin_1: Se esperaba un identificador
Procedure is ....;
----------^
La sintaxis que aquí se utiliza para la salida de errores sintácticos es: entre paréntesis
el número identificativo que el analizador asigna al error; empieza por 1 y se
incrementa correlativamente (de momento, pararemos en cuanto encontremos uno;
cuando implementemos recuperación de errores, podrán detectarse varios en una
misma pasada).
A continuación lin_#, donde # representa el número de la línea donde se ha detectado
el error. Después información referente al error detectado. Esta información debe ser
tan precisa y útil para el usuario como sea posible a continuación, en la línea siguiente,
el texto que se lleva reconocido de la línea que se está procesando en la línea
siguiente, un apuntador a la posición precisa donde se ha localizado el error.
Es preciso que tener en cuenta que para que el último apartado sea viable, es
necesario saber a cuántos espacios en blanco ha de equivaler un tabulador cuando se
escribe el mensaje de error. Para ello, la invocación a la aplicación será como sigue:
AS_LL1 3 nombreDelFicheroConElFuente
donde 3 indica el número de espacios a que equivale un tabulador (habitualmente, la
salida estándar está configurada, por defecto, a 8 espacios en blanco por tabulador).
Notar que será necesario que hacer una nueva versión del analizador léxico para poder
ir almacenando el texto de la línea que se está procesando de acuerdo con las nuevas
especificaciones. Suponer para ello que ningún programador con sentido común
utilizaría nunca una línea de más de 132 caracteres.
- 30 -
Observaciones
Para establecer la gramática de miLenguaje será de ayuda consultar las que aparecen
en los libros de C, Ada, Pascal, etc. Suele ser especialmente crítica la parte de la
gramática correspondiente a las expresiones, por lo que es importante que esta parte
se trate con cuidado. No construir toda la gramática de una sola vez. Probar a hacerlo
de una manera incremental. Construir primero la gramática que sólo reconozca la
declaración del procedimiento/función y su cuerpo. Progresivamente, ir añadiendo
nuevas reglas, realizando tests, y así sucesivamente. La gramática completa de un
lenguaje de las características del que estamos manejando ocupa alrededor de tres
páginas de fuente Yacc. Su transformación para el análisis LL(1) ocupará un poco más
(pero no mucho más).
3.4.3
Recuperación de errores en analizadores sintácticos descendentes
Funciones de un gestor de errores:
_ Determinar si el programa es sintácticamente correcto (reconocedor).
_ Proporcionar un mensaje de error significativo.
parser error: line 10 column 4, found simbol _ expected simbol ;
_ Reanudar el análisis tan pronto como sea posible. Si no podemos perder demasiada
parte de la entrada.
_ Evitar errores en cascada, un error genera una secuencia de sucesivos errores
espurios. Para evitarlo se debe consumir (ignorar parte de la entrada).
_ Realizar una corrección (reparación) del error. El analizador intenta inferir un
programa correcto de uno incorrecto. Corrección de mínima distancia (el número de
tokens que deben ser insertados, borrados o cambiados debe ser mínimo).
Extremadamente complicado “adivinar” la intención del programador. Sólo útil para
situaciones sencillas como: “falta ;”. Los métodos de recuperación de errores suelen
ser métodos “ad-hoc”, en el sentido de que se aplican a lenguajes específicos y a
métodos concretos de análisis sintáctico (descendente, ascendente. etc), con muchas
situaciones particulares.
Para emitir un mensaje significativo de error se usa el conjunto de símbolos de
predicción definidos para cada no-terminal como:
Dada A→ 𝛼,
si 𝜀 ∈ 𝑃𝑅𝐼𝑀 (A) entonces PRED (A) = PRIM (A) – {𝜀} U SIG (A)
sino PRED (A) = PRIM (A).
Recuperación en modo de pánico: se ignoran símbolos de la entrada hasta encontrar
un componente léxico con el cual reanudar el análisis. En el peor caso se podría haber
consumido por completo el resto del fichero que queda por analizar (panic mode), en
cuyo caso no sería mejor que simplemente hacer terminar con el análisis al detectar el
error. Si se implementa con cuidado puede ser mejor que lo que su nombre implica
(N. Wirth en su intento de mejorar su imagen le llama don’t panicmode).
3.5
Perspectiva general del análisis sintáctico ascendente
Análisis ascendente: se construye el árbol de análisis sintáctico de la cadena
de entrada desde las hojas hasta la raíz. En las hojas tenemos la cadena a
analizar (los símbolos terminales) que se intentan reducir al axioma, que
- 31 -
se encontrará en la raíz, si la cadena es correcta sintácticamente.
¿Cómo construir el árbol?
Se trata de desplazarse en la entrada hasta encontrar una sub cadena de
símbolos que represente la parte derecha de una producción, en ese
momento sustituimos esa sub cadena por el no-terminal de la parte
izquierda correspondiente de la producción, la reducimos.
3.5.1
Autómatas finitos de elementos LR(0)y análisis sintáctico LR(0)
La tabla de transiciones del autómata nos genera dos tablas: la tabla de acciones y la
de saltos. El algoritmo de análisis sintáctico LR en el que se basa yapp utiliza una pila y
dos tablas para analizar la entrada. Como se ha visto, la tabla de acciones contiene
cuatro tipo de acciones:
Desplazar (shift)
Reducir (reduce)
Aceptar
Error
El algoritmo utiliza una pila en la que se guardan los estados del autómata. De este
modo se evita tener que ``comenzar'' el procesado de la forma sentencia derecha
resultante después de una reducción (anti derivación).
Algoritmo Análizador LR
push(s0);
b = yylex();
for( ; ; ;) {
s = top(0); a = b;
switch (action[s][a]) {
case "shift t" :
push(t);
b = yylex();
break;
case "reduce A ->alpha" :
eval(Sub{A -> alpha}->(top(|alpha|-1).attr, ... , top(0).attr));
pop(|alpha|);
push(goto[top(0)][A]);
break;
case "accept" : return (1);
default : yyerror("syntax error");
}
}
Como es habitual, |X| denota la longitud de la cadena . La función top(k) devuelve el
elemento que ocupa la posición k desde el top de la pila (esto es, está a profundidad
k). La función pop(k) extrae k elementos de la pila. La notación state.attr hace
referencia al atributo asociado con cada estado. Denotamos por sub_{reduce A ->
alpha} el código de la acción asociada con la regla
.
Todos los analizadores LR comparten, salvo pequeñas excepciones, el mismo algoritmo
de análisis. Lo que más los diferencia es la forma en la que construyen las tablas. En
yapp la construcción de las tablas de acciones y gotos se realiza mediante el algoritmo
LALR.
- 32 -
3.5.2
Análisis sintáctico SLR(1)
Después de que se estudia la operación de un analizador sintáctico LR, se deben
revisar las técnicas para construir una tabla de análisis sintáctico LR para una
gramática. El primer método, llamado LR simple o SLR por sus siglas en inglés, es el
mas fácil de implementar, pero también es el menos poderoso.
3.5.3
Yacc: un generador de analizadores sintácticos LALR(1)
El método para implementar una tabla de análisis sintáctico con examen por anticipado LALR por
sus siglas en inglés, es considerado de poder mediano, pero también de costo razonable. Este
método funciona con las gramáticas de la mayoría de los lenguajes de programación y con algo de
trabajo es posible hacerlo muy eficiente.
Yacc es un programa informático muy común en los sistemas Unix. Se trata de un
generador de analizadores sintácticos. Las siglas del nombre significan "Yet Another
Compiler Compiler". Genera un analizador sintáctico (la parte de un compilador que
intenta darle sentido a la entrada) basado en una gramática analítica escrita en una
notación similiar a la BNF. Yacc genera el código para el analizador sintáctico en el
Lenguaje de programación C. Fue desarrollado por Stephen C. Johnson en AT&T para
el sistema operativo de Unix. Después se escribieron programas compatibles, por
ejemplo Berkeley Yacc, GNU bison, MKS yacc y Abraxas yacc (una versión actualizada
de la versión original de AT&T que también es software libre como parte del proyecto
de OpenSolaris de Sun). Cada una ofrece mejoras leves y características adicionales
sobre el Yacc original, pero el concepto ha seguido siendo igual. Yacc también se ha
reescrito para otros lenguajes, incluyendo Ratfor, EFL, ML, Ada, Java, y Limbo.
Puesto que el analizador sintáctico generado por Yacc requiere un analizador léxico, se
utiliza a menudo conjuntamente con un generador de analizador léxico, en la mayoría
de los casos lex o Flex alternativa del software libre. El estándar de IEEE POSIX
P1003.2 define la funcionalidad y los requisitos a Lex y Yacc. La versión Yacc de AT&T
se convirtió en software libre; el código fuente está disponible con las distribuciones
estándares del Plan 9 y de OpenSolaris.
En el siguiente esquema se muestra, de una manera simplificada, lo que constituye la
entrada y la salida para el traductor/generador Yacc.
Especificación
Programa generado
..
..
.
..
.
yyparse
yacc
(eSint.y)
(y.tab.c)
- 33 -
La entrada a Yacc es una especificación sintáctica (estructural) escrita en forma de
gramática de contexto independiente (codificada de manera semejante a la notación
BNF-No ampliada). Las gramáticas son mecanismos adecuados para la especificación
de las características sintácticas de los lenguajes de programación y se representan
con una notación textual fácil de procesar; por estos motivos resultan apropiadas para
emplearlas como entrada a Yacc para generar analizadores sintácticos.
La salida es un programa escrito en C cuya parte central es una función llamada
yyparse que realiza el análisis sintáctico de la secuencia de piezas sintácticas recibidas.
El algoritmo incorporado a la función lleva a cabo un análisis sintáctico ascendente por
desplazamiento y reducción, según el método LALR(1).
3.5.4
Recuperación de errores en analizadores sintácticos ascendentes
Un compilador es un sistema que en la mayoría de los casos tiene que manejar una
entrada incorrecta. Sobre todo en las primeras etapas de la creación de un programa,
es probable que el compilador se utilizará para efectuar las características que debería
proporcionar un buen sistema de edición dirigido por la sintaxis, es decir, para
determinar si las variables han sido declaradas antes de usarla, o si faltan corchetes o
algo así. Por lo tanto, el manejo de errores es parte importante de un compilador y el
escritor del compilador siempre debe tener esto presente durante su diseño. Hay que
señalar que los posibles errores ya deben estar considerados al diseñar un lenguaje de
programación. Por ejemplo, considerar si cada proposición del lenguaje de
programación comienza con una palabra clave diferente (excepto la proposición de
asignación, por supuesto). Sin embargo, es indispensable lo siguiente:
1. El compilador debe ser capaz de detectar errores en la entrada;
2. El compilador debe recuperarse de los errores sin perder demasiada
información;
3. Y sobre todo, el compilador debe producir un mensaje de error que permita al
programador encontrar y corregir fácilmente los elementos (sintácticamente)
incorrectos de su programa. Los mensajes de error de la forma:
*** Error 111 ***
*** Ocurrió un error ***
*** Falta declaración ***
*** Falta delimitador ***
no son útiles para el programador y no deben presentarse en un ambiente de compilación
amigable y bien diseñado. Por ejemplo, el mensaje de error ‘Falta declaración’ podría
reemplazarse por *** No se ha declarado la variable Nombre ***, o en el caso del delimitador
omitido se puede especificar cuál es el delimitador esperado. Además de estos mensajes de error
informativos, es deseable que el compilador produzca una lista con el código fuente e indique en
ese listado dónde han ocurrido los errores.
No obstante, antes de considerar el manejo de errores en el análisis léxico y sintáctico, hay que
caracterizar y clasificar los errores posibles (Sec. 6.1). Esta clasificación nos mostrará que un
compilador no puede detectar todos los tipos de errores.
- 34 -
Clasificación de Errores
Durante un proceso de resolución de problemas existen varias formas en que pueden
surgir errores, las cuales se reflejan en el código fuente del programa. Desde el punto
de vista del compilador, los errores se pueden dividir en dos categorías:
1. Errores visibles y Errores invisibles
Los errores invisibles en un programa son aquellos que no puede detectar el
compilador, ya que no son el resultado de un uso incorrecto del lenguaje de
programación, sino de decisiones erróneas durante el proceso de especificación o de la
mala formulación de algoritmos. Por ejemplo, si se escribe
a : = b + c ; en lugar de a : = b * c ;
el error no podrá ser detectado por el compilador ni por el sistema de ejecución. Estos
errores lógicos no afectan la validez del programa en cuanto a su corrección sintáctica.
Son objeto de técnicas formales de verificación de programas que no se consideran
aquí. Para conocer más sobre la verificación de programas, consulte, por ejemplo,
[LOEC 87].
Los errores visibles, a diferencia de los errores lógico, pueden ser detectados por el
compilador o al menos por el sistema de ejecución. Estos errores se pueden
caracterizar de la siguiente manera:
1. Errores de ortografía y
2. Errores que ocurren por omitir requisitos formales del lenguaje de
programación.
Estos errores se presentará porque los programadores no tienen el cuidado suficiente
al programador. Los errores del segundo tipo también pueden ocurrir porque el
programador no comprende a la perfección el lenguaje que se utiliza o porque suele
escribir sus programas en otro lenguaje y, por tanto, emplea las construcciones de
dicho lenguaje (estos problemas pueden presentarse al usar a la vez lenguajes de
programación como PASCAL y MODULA-2, por ejemplo).
Clasificación de Ocurrencias
Por lo regular, los errores visibles o detectables por el compilador se dividen en tres
clases, dependiendo de la fase del compilador en el cual se detectan:
1. Errores Léxicos;
2. Errores Sintácticos;
3. Errores Semánticos;
Por ejemplo, un error léxico puede ocasionarse por usar un carácter inválido (uno que
no pertenezca al vocabulario del lenguaje de programación) o por tratar de reconocer
una constante que produce un desbordamiento.
- 35 -
Un error de sintaxis se detecta cuando el analizador sintáctico espera un símbolo que
no corresponde al que se acaba de leer. Los analizadores sintácticos LL y LR tienen la
ventaja de que pueden detectar errores sintácticos lo más pronto posible, es decir, se
genera un mensaje de error en cuanto el símbolo analizado no sigue la secuencia de
los símbolos analizados hasta ese momento.
Los errores semánticos corresponden a la semántica del lenguaje de programación, la
cual normalmente no está descrita por la gramática. Los errores semánticos más
comunes son la omisión de declaraciones. Además de estas tres clases de errores, hay
otros que serán detectados por el sistema de ejecución porque el compilador ha
proporcionado el código generado con ciertas acciones para estos casos.
1. Un Error de Ejecución
típico ocurre cuando el índice de una matriz no es un elemento del subintervalo
especificado o por intentar una división entre cero. En tales situaciones, se
informa del error y se detiene la ejecución del programa.
Clasificación Estadística
Ripley y Druseikis muestran resultados interesantes sobre el análisis estadístico
de los errores de sintaxis en [RIPL 78]. Ellos investigaron los errores que
cometen los programadores de PASCAL y analizaron los resultados en relación
con las estrategias de recuperación. El resultado principal del estudio fue que
los errores de sintaxis suelen ser muy simples y que, por lo general, sólo ocurre
un error por frase. En el resumen siguiente se describen de manera general los
resultados del estudio:
2. Al menos el 40% de los programas compilados eran
semánticamente incorrectos.
3. Un 80% de las proposiciones incorrectas sólo tenían un error.
sintáctica
o
4. El 13% de las proposiciones incorrectas tenían dos errores, menos del 3%
tenían tres errores y el resto tenían cuatro o más errores por proposición.
Figura 3.4
- 36 -
5. En aproximadamente la mitad de los errores de componentes léxicos olvidados,
el elemento que faltaba era ":", mientras que omitir el "END" final ocupaba el
segundo lugar, con un 10.5%.
6. En un 13% de los errores de componentes léxico incorrecto se escribió "," en
lugar de ";" y en más del 9% de los casos se escribió ":=" en lugar de "=".
Los errores que ocurren pueden clasificarse en cuatro categorías:
1.
2.
3.
4.
Errores de puntuación,
Errores de operadores y operandos,
Errores de palabras clave y
Otros tipos de errores.
La distribución estadística de estas cuatro categorías aparece en la figura 6.1.
Efectos de los Errores
La detección de un error en el código fuente ocasiona ciertas reacciones del
compilador. El comportamiento de un compilador en el caso de que el código fuente
contenga un error puede tener varias facetas:
1. El proceso de compilación de detiene al ocurrir el error y el compilador debe
informar del error.
2. El proceso de compilación continúa cuando ocurre el error y se informa de éste
en un archivo de listado.
3. El compilador no reconoce el error y por tanto no advierte al programador.
La última situación nunca debe presentarse en un buen sistema de compilación; es
decir, el compilador debe ser capaz de detectar todos los errores visibles.
La detención del proceso de compilación al detectar el primer error es la forma más
simple de satisfacer el requisito de que una compilación siempre debe terminar, sin
importar cuál sea la entrada [BRIN 85]. Sin embargo, este comportamiento también es
el peor en un ambiente amigable para el usuario, ya que una compilación puede tardar
varios minutos. Por lo tanto, el programador espera que el sistema de compilación
detecte todos los errores posibles en el mismo proceso de compilación.
Entonces, en general, el compilador debe recuperarse de un error para poder revisar el
código fuente en busca de otros errores. No obstante, hay que observar que cualquier
"reparación" efectuada por el compilador tiene el propósito único de continuar la
búsqueda de otros errores, no de corregir el código fuente. No hay reglas generales
bien definidas acerca de cómo recuperarse de un error, por lo cual el proceso de
recuperación debe hacerse en hipótesis acerca de los errores. La carencia de tales
reglas se debe al hecho de que el proceso de recuperación siempre depende del
lenguaje.
- 37 -
Manejo de Errores en el Análisis Léxico
Los errores léxicos se detectan cuando el analizador léxico intenta reconocer
componentes léxicos en el código fuente. Los errores léxicos típicos son:
1. Nombres ilegales de identificadores: un nombre contiene caracteres inválidos;
2. Números inválidos: un número contiene caracteres inválidos (por ejemplo; 2,13
en lugar de 2.13), no está formando correctamente (por ejemplo, 0.1.33), o es
demasiado grande y por tanto produce un desbordamiento;
3. Cadenas incorrectas de caracteres: una cadena de caracteres es demasiado
larga (probablemente por la omisión de comillas que cierran);
4. Errores de ortografía en palabras reservadas: caracteres omitidos, adicionales,
incorrectos o mezclados;
5. Etiquetas ilegales: una etiqueta es demasiado larga o contiene caracteres
inválidos;
6. Fin de archivo: se detecta un fin de archivo a la mitad de un componente léxico.
La mayoría de los errores léxicos se deben a descuidos del programador. En general, la
recuperación de los errores léxicos es relativamente sencilla.
Si un nombre, un número o una etiqueta contiene un carácter inválido, se elimina el
carácter y continúa el análisis en el siguiente carácter; en otras palabras, el analizador
léxico comienza a reconocer el siguiente componente léxico. El efecto es la generación
de un error de sintaxis que será detectado por el analizador sintáctico. Este método
también puede aplicarse a números mal formados.
Las secuencias de caracteres como 12AB pueden ocurrir si falta un operador (el caso
menos probable) o cuando se han tecleado mal ciertos caracteres. Es imposible que el
analizador léxico pueda decidir si esta secuencia es un identificador ilegal o u número
ilegal. En tales casos, el analizador léxico puede saltarse la cadena completa o intentar
dividir las secuencias ilegales en secuencias legales más cortas. Independientemente
de cuál sea la decisión , la consecuencia será un error de sintaxis.
La detección de cadenas demasiado margas no es muy complicada, incluso si faltan las
comillas que cierran, porque por lo general no está permitido que las cadenas pasen de
una línea a la siguiente. Si faltan las comillas que cierran, puede usarse el carácter de
fin de línea como el fin de cadena y reanudar el análisis léxico en la línea siguiente.
Esta reparación quizás produzca errores adicionales. En cualquier caso, el programador
debe ser informado por medio de un mensaje de error.
Un caso similar a la falta de comillas que cierran en una cadena, es la falta de un
símbolo de terminación de comentario. Como por lo regular está permitido que los
comentario abarquen varias líneas, no podrá detectarse la falta del símbolo que cierra
el comentario hasta que el analizador léxico llegue al final del archivo o al símbolo de
fin de otro comentario (si no se permiten comentarios anidados).
Si se sabe que el siguiente componente léxico debe ser una palabra reservada, es
posible corregir una palabra reservada mal escrita. Esto se hace mediante funciones de
corrección de errores, bien conocidas en los sistemas de lenguajes naturales, o
simplemente aplicando una función de distancia métrica entre la secuencia de entrada
y el conjunto de palabras reservadas.
- 38 -
Por último, el proceso de compilación puede terminar si se detecta un fin de archivo
dentro de un componente léxico.
Manejo de Errores en el Análisis Sintáctico
El analizador sintáctico detecta un error de sintaxis cuando el analizador léxico
proporciona el siguiente símbolo y éste es incompatible con el estado actual del
analizador sintáctico. Los errores sintácticos típicos son:
1. Paréntesis o corchetes omitidos, por ejemplo, x : = y * (1 + z;
2. Operadores u operando omitidos, por ejemplo, x : = y (1 + z );
3. Delimitadores omitidos, por ejemplo, x : = y + 1 IF a THEN y : = z.
No hay estrategias de recuperación de errores cuya validez sea general, y la mayoría
de las estrategias conocidas son heurísticas, ya que se basan en suposiciones acerca
de cómo pueden ocurrir los errores y lo que probablemente quiso decir el programador
con una determinada construcción. Sin embargo, hay algunas estrategias que gozan
de amplia aceptación:
1. Recuperación de emergencia (o en modo pánico): Al detectar un error, el
analizador sintáctico salta todos los símbolos de entrada hasta encontrar un
símbolo que pertenezca a un conjunto previamente definido de símbolos de
sincronización. Estos símbolos de sincronización son el punto y como, el símbolo
end o cualquier palabra clave que pueda ser el inicio de una proposición nueva,
por ejemplo. Es fácil implantar la recuperación de emergencia, pero sólo
reconoce un error por proporción. Esto no necesariamente es una desventaja,
ya que no es muy probable que ocurran varios errores en la misma proposición
(véase [IPL 78], por ejemplo). Esta suposición es un ejemplo típico del carácter
heurístico de esta estrategia.
2. Recuperación por inserción, borrado y reemplazo: éste también es un método
fácil de implantar y funciona bien en ciertos casos de error. Usemos como
ejemplo una declaración de variable en PASCAL . cuando una coma va seguida
por dos puntos, en lugar de un nombre de variable, es posible eliminar esta
coma. En forma similar, se puede insertar un punto y coma omitido o
reemplazar un punto y coma por una coma en una lista de parámetros.
3. Recuperación por expansión de gramática: De acuerdo con [RIPL 78], el 60%
de los errores en los programas fuente son errores de puntuación, por ejemplo,
la escritura de un punto y coma en lugar de una coma, o viceversa. Una forma
de recuperarse de estos errores es legalizarlos en ciertos casos, introduciendo
lo que llamaremos producciones de error en la gramática del lenguaje de
programación. La expansión de la gramática con estas producciones no quiere
decir que ciertos errores no serán detectados, ya que pueden incluirse acciones
para informar de su detección.
La recuperación de emergencia es la estrategia que se encontrará en la mayoría de los
compiladores, pero la legalización de ciertos errores mediante la definición de una
gramática aumentada es una técnica que se emplea con frecuencia. No obstante, hay
que expandir la gramática con mucho cuidado para asegurarse de que no cambien el
tipo y las características de la gramática.
Los errores de sintaxis se detectan cuando el analizador sintáctico espera un símbolo
que no concuerda con el símbolo que está analizando, a . En los analizadores
- 39 -
sintácticos LL, los errores de sintaxis se detectan cuando a y el no terminal que están
en la cima de la pila nos llevan a un índice de una posición vacía de la tabla de análisis
sintáctico. En los analizadores sintácticos LR, los errores de sintaxis se detectan
cuando hay un índice a una posición vacía de la tabla, o sea, cuando no se especifica
ninguna transición al analizar á en el estado actual (véase Cap. 4). Sin embargo, si se
emplea una gramática aumentada con producciones de error adicionales, no sólo se
detectarán errores por medio de los índices a posiciones vacías de la tabla de análisis
sintáctico.
Errores Semánticos
Los errores que puede detectar el analizador sintáctico son aquellos que violan las
reglas de una gramática independiente del contexto. Ya hemos mencionado que
algunas de las características de un lenguaje de programación no pueden enunciarse
con reglas independientes del contexto, ya que dependen de él; por ejemplo, la
restricción de que los identificadores deben declararse previamente. Por lo tanto, los
principales errores semánticos son:
1. Identificadores no definidos;
2. Operadores y operandos incompatibles.
Es mucho más difícil introducir métodos formales para la recuperación de errores
semánticos que para la recuperación de errores sintácticos, ya que a menudo la
recuperación de errores semánticos es ad hoc. No obstante, puede requerirse que, por
lo menos, el error semántico sea informado al programador, que se le ignore y que,
por tanto, se suprimirá la generación de código.
Sin embargo, la mayoría de los errores semánticos pueden ser detectados mediante la
revisión de la tabla de símbolos, suponiendo un tipo que se base en el contexto donde
ocurra o un tipo universal que permita al identificador ser un operando de cualquier
operador del lenguaje. Al hacerlo, evitamos la producción de un mensaje de error cada
vez que se use la variable no definida. Si el tipo de un operando no concuerda con los
requisitos de tipo del operador, también es conveniente reemplazar el operando con
una variable ficticia de tipo universal.
Recuperación de Errores PL/0
A continuación ejemplificaremos algunos de los métodos antes mencionados para la
recuperación de errores sintácticos. Para ellos expandiremos fragmentos del programa
del analizador sintáctico descendente recursivo de PL/0 que vimos en el capítulo 4.
Recuperación de Emergencia
La idea del análisis sintáctico descendente recursivo es que un problema de análisis
sintáctico se divida en subproblemas que se resuelven en forma recursiva. Ahora bien,
la ocurrencia de un error en un subproblema significa que no sólo hay que informar del
error al procedimiento que llama. Mas bien, hay que garantizar que el procedimiento
del subproblema se recupere del error de modo que el procedimiento invocador pueda
continuar con el proceso de análisis sintáctico, es decir, que termine de forma normal.
- 40 -
Por ello, además de generar un mensaje de error, hay que ir saltándose la entrada
hasta llegar a un símbolo de sincronización. Esto implica que cada procedimiento de un
analizador sintáctico descendente recursivo debe conocer cuáles son los símbolos
PROCEDURE Prueba(Siguiente, detención: conjsím; n:
INTEGER);
(*siguiente, detención: símbolos de sincronización*)
(*n: número de error *)
VAR símsinc : conjsím;
BEJÍN
IF NOT (símbolo IN siguiente) THEN
Error (n);
Símsinc := siguiente + detención;
WHILE NOT (símbolo IN símsinc) DO Leer_Símbolo END
END
END Prueba;
Figura 6.2 Procedimiento para revisar y saltar símbolos
PROCEDURE Expresión (siguiente: conjsím);
VAR ADDoSUB: símbolos;
PROCEDURE Término (siguiente: conjsím);
VAR MULoDIV:símbolos;
PROCEDURE Factor (siguiente: conjsím);
VAR i: INTEGER;
BEGÍN (*Factor*)
Prueba (iniciofact, siguiente, ...);
WHILE símbolo IN iniciofact DO
...
Prueba (siguiente, [pareni], ...)
END
END Factor;
BEGÍN (*Término*)
Factor (siguiente + [por, diagonal]);
WHILE símbolo IN [por, diagonal]) DO
MULoDIV := símbolo; Leer_Símbolo;
Factor (siguiente + [por, diagonal]);
...
END
END Término;
BEGÍN (*Expresión*)
...
END Expresión;
válidos que le pueden seguir. Para evitar el salto descontrolado de símbolos, se
aumentan los conjuntos de símbolos de detención adicionales que indiquen las
construcciones que no deben saltarse. Los símbolos siguientes y los símbolos de
detención forman, en conjunto, los símbolos de sincronización.
- 41 -
En la caso de la implantación, esto quiere decir que cada procedimiento de análisis
sintáctico consta de un parámetro que especifica el conjunto de los símbolos válidos
que siguen. La prueba para los símbolos de sincronización puede efectuarse fácilmente
con el procedimiento presentado en la figura 6.2. este procedimiento prueba si un
símbolo siguiente es legal. En caos de un símbolo ilegal, se genera un mensaje de
error y se saltan los símbolos de entrada hasta detectar un símbolo de sincronización.
Este procedimiento de prueba será invocado al final de cada procedimiento para
verificar que le símbolo siguiente sea válido, pero también puede emplearse al iniciar
un procedimiento de análisis sintáctico para verificar si el símbolo de entrada actual es
un símbolo inicial permitido. El uso del procedimiento de prueba se ilustra en la figura
6.3 para el análisis sintáctico de expresiones aritméticas (donde ‘iniciofact’ indica los
símbolos iniciales permitidos para ‘Factor’).
Expansión de Gramática
Como ya mencionamos, es un hecho bien conocido que los errores de puntuación son
muy comunes. Por ejemplo, consideremos las constantes PL/0 que se separan por
comas; un error frecuente en el cual podríamos pensar sería el uso de un punto y
coma en lugar de la coma. Sabiendo esto, la estructura sintáctica de las declaraciones
de constantes puede modificarse de manera que se permita usar coma y punto y
coma, como se muestra en la figura 6.4.
La declaración modificada de constantes de la figura 6.4 legaliza el error que acabamos
de describir. El diagrama sintáctico de la figura 6.4 puede entonces traducirse al
fragmento de programa de la figura 6.5, mediante las técnicas presentadas en el
capítulo 4.
Figura 3.5
IF símbolo = símconst THEN
Leer_Símbolo;
REPEAT
Declaración_const;
WHILE símbolo = coma DO
Leer_Símbolo; Declaración_const
END;
IF símbolo = puntocoma THEN Leer_Símbolo
- 42 -
ELSE Error(...) END;
UNTIL (símbolo <> ident);
END;
El fragmento del programa de la figura 6.5 permite la separación de constantes con
comas o puntos y coma sin producir mensajes de error. Además de esta legalización,
se aceptará la omisión de la coma y el punto y coma; sin embargo, en este caso sí se
produce un mensaje de error. Es obvio que de esta misma forma podemos expandir la
sintaxis de las declaraciones de variables para permitir la separación con puntos y
coma o incluso con espacios (véase Fig. 6.6).
IF símbolo = símvar THEN
Ler_Símbolo;
REPEAT
Declaración_var;
WHILE símbolo = coma DO
Leer_Símbolo; Declaración_var
END;
IF símbolo = puntocoma THEN Leer_Símbolo
ELSE Error (...) END;
UNTIL (símbolo <> ident);
END;
En forma análoga, puede permitirse la omisión del punto y coma entre dos
proposiciones. Esto muestra en el fragmento de programa siguiente, donde
‘síminicioprop’ es el conjunto de símbolos iniciales de la proposiciones.
IF símbolo = símbegin THEN
Leer_Símbolo;
REPEAT
Proposición (siguiente + [puntocoma, símend]);
WHILE símbolo = puntocoma DO
Leer_Símbolo;
Proposición (siguiente + [puntocoma, símed]);
END
UNTIL NOT (símbolo IN síminicioprop);
IF símbolo = símed THEN
Leer_símbollo
ELSE Error(...) END;
END;
- 43 -
Descargar