Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Ejemplo.
Programa X1;
Var X, Y: Integer;
Z: Real;
Arreglo: Array [1…100] of int
Procedure compara (a, b: Integer)
Var n, m: integer;
Begin
----
End
Begin
----
End
• Patrones de tokens que no coincidan con algún patrón válido. Por ejemplo
el token #### sería inválido en algunos L.P.
• Caracteres inválidos para el alfabeto del el lenguaje.
• Longitud de ciertos tokens demasiado larga.
• Generadores de Código Léxico
• La construcción de un scanner puede automatizarse por medio de un
generador de analizadores de léxico.
• El primer programa de este tipo que se hizo popular fue Lex (Lesk, 1975)
que generaba un scanner en lenguaje C.
Hoy en día existen muchos programas de este tipo: Flex, Zlex, YooLex, JavaCC,
SableCC, etc.
Ejemplo: Javacc
Javacc (Java Compiler Compiler) es un generador de scanners y parsers
(https://javacc.dev.java.net/).
En los dos ejemplos vistos en sección 4.1, los dos árboles de parsing para 1-2-3
significaba diferentes cosas: (1-2)-3=-4 versus 1-(2-3)=2. Similarmente, (1+2)x3 no
es lo mismo que 1+(2x3). Es por eso que gramáticas ambiguas son problemáticas
para un compilador. Afortunadamente, una gramática ambigua puede convertirse
en una no ambigua. Por ejemplo, si queremos encontrar una gramática no
ambigua para el lenguaje de la segunda gramática de sección 4.1, lo podemos
hacer por medio de dos operaciones: primero, aplicar orden de precedencia a los
operadores y segundo aplicar asociatividad por la izquierda. Con esto, tenemos la
sig. gramática:
El parser es el programa que funciona como núcleo del compilador. Alrededor del
parser funcionan los demás programas como el scanner, el analizador semántico y
el generador de código intermedio. De hecho se podría decir que el parser
comienza el proceso de compilación y su primer tarea es pedir al escáner que
envíe los tokens necesarios para llevar a cabo el análisis sintáctico, del estatuto,
expresión o declaración dentro de un programa.
También el parser llama rutinas del análisis semántico para revisar si el significado
del programa es el correcto.
Por ultimo el parser genera código intermedio para los estatutos y expresiones si
no se encontraron errores en ellos.
Existen diferentes técnicas o métodos para realizar un análisis sintáctico “Parsing”.
Estas técnicas se dividen en dos tipos:
Descendentes
Ascendentes
Por otra parte las técnicas ascendentes realizan su análisis partiendo de las hojas
o tokens y mediante una serie de operaciones llamadas reducciones terminan en
la raíz o símbolo inicial de la gramática.
Por lo general las técnicas descendentes son mas sencillas de implementar que
las ascendentes, pero por otra parte son menos eficientes
Un parser Predictivo es aquel que “ predice ” que camino tomar al estar realizando
el análisis sintáctico. Esto quiere decir que el parser nunca regresara a tomar otra
decisión ( back tracking ) para irse por otro camino, como sucede en las
derivaciones. Para poder contar con un parser predictivo la gramática debe de
tener ciertas características, entre ellas la mas importante es que el primer
símbolo de la parte derecha de cada producción le proporcione suficiente
información al parser para que este escoja cual producción usar. Normalmente el
primer símbolo mencionado es un Terminal o token.
Esta técnica se utilizó o popularizó en los años 70 a partir del primer compilador de
pascal implementado con ella. A continuación ilustraremos esto escribiendo un
parser recursivo descendente para la siguiente gramática:
S à if E then S else S
S àbegin S L
S àprint E
L àend
L à; S L
E ànum = num
Sin embargo también el parser ayuda a realizar esta tarea pues el es quien llama
a las respectivas rutinas semánticas para que realicen funciones relacionada con
la tabla de símbolos
Dentro del código del parser predictivo estudiado en clase se puede observar que
el parser llama un metodo “error” para el manejo de errores sintácticos, que es
cuando el parser obtiene un token no esperado.
¿Cómo se maneja el error para esta clase de parsers? Una forma simple es
ejecutar una excepción y parar la compilación. Esto como que no es muy amigable
par el usuario.
Ejemplo
}}
Al igual que en los lenguajes naturales (español, ingles, etc.) en los lenguajes de
programación existen reglas semánticas para definir el significado de los
programas, estatutos, expresiones, etc.
Por ejemplo un error semántico es usar (en pascal ó java) un identificador que no
fue anteriormente declarado.
Cuando mezclamos diferentes tipos en una misma expresión o que llamamos una
rutina que no existe existe un error semántico.
Una de las funciones del analizador smántico es verificar que los tipos de una
expresión sean compatibles entre si.
Para hacer lo anterior el compilador cuenta con información de los atributos (tipos,
tamaño, número de argumento, etc.) de los identificadores en una tabla de
símbolos
a = b * c;
El compilador algunas veces con ciertos diferentes tipos puede hacer una
conversión interna en forma implícita para solucionar el problema. Otras veces el
programador explícitamente es el que hace la conversión (casting).
Ejemplo:
float dinero;
int cambio;
Cada símbolo terminal y noterminal puede asociarse con su propio tipo de valor
semántico.
Como fue visto en el capitulo anterior (4), un parser ascendente utiliza durante el
análisis una pila. En esta va guardando datos que le permiten ir haciendo las
operaciones de reducción que necesita.
El análisis semántico conecta las definiciones de las variables con sus usos, checa
que cada expresión tenga un tipo correcto y traduce la sintaxis abstracta a una
representación mas simple para generar código máquina.
Cada variable local en un programa tiene un ámbito (scope) dentro del cual es
visible. Por ejemplo, en un método MiniJava m, todos los parámetros formales y
variables locales declarados en m son visibles solo hasta que finalice m.
Esto debido a que normalmente el programador prefiere que le describan todos los
errores posibles del programa fuente.
Hasta esta etapa (chequeo de tipos), la parte del compilador se conoce con el
nombre de “front End”.
Lenguajes intermedios
Los arboles sintácticos ascendentes o descendentes y la notación posfija son dos
clases de representación de código intermedio.
Notación infija
Es la notación habitual. El orden de su recorrido es el siguiente
1er operando, operador, segundo operando, cuando se realiza el recorrido de una
expresión con este tipo de notación, lo único que cambia es la eliminación de
paréntesis
Expresión: (2+(3*4))
Recorrido: 2+ 3*4
Notación posfija
La notación posfija E se puede definir como:
1.- Si E es una variable o una cte. Entonces la notación posfija de E es también E
2.- Si E es una expresión de la forma:
E1 op E2, Donde op es cualquier operando binario, entonces la notación
posfija de E es E1’ E2’ op
3.- Si E es una expresión de la forma (E1) entonces la notación posfija de E
Otra forma de definir una expresión escrita en notación posfija seria escribiendo la
siguiente gramática:
Ejemplo:
Exp= (4*X)+3 = 4X*3+
Notación prefija
Esta notación es similar a la anterior pero ahora es descrita por la siguiente
gramática:
Notación Cuartetos
Los cuartetos son la forma de código intermedio mas usado, este lenguaje
consiste de una secuencia de cuartetos posiblemente precedida de una etiqueta,
donde cada cuarteto puede tener las formas siguientes:
Del x + p; * x := y;
Dep x + p; x [y] :=z;
X := y opb z; goto etiy;
X := opu y; if not v goto etiq;
X := y; param x;
X :=z[y]; call p, n
X:=*y ; return ; return (x;);
Notación tercetos
Es una notación similar a la de los cuartetos pero mas compacta aquí cada terceto
puede tener asociado un resultado temporal. Este resultado temporal puede ser
referenciado por un terceto posterior
(1) / b c
(2) = a (1)
(3) X c d
(4) = d (3)
Notación tercetos indirecta
Es similar a la anterior se añade un arreglo que indica el orden en que se
ejecutaran los tercetos
(1) / b c
(3) = a (1)
(2) X c d
(4) = d (2)
Ejemplo:
VII Optimización
Ejemplo:
while(a == b)
{
int c = a; c = 5;
…
}
En este caso es mejor pasar el int c =a; fuera del ciclo de ser posible.
Existen algunas herramientas que permiten el análisis de los flujos de datos, entre
ellas tenemos los depuradores y ensambladores, que sigue paso a paso todo el
desarrollo del compilador.
Desventajas
a) Imposibilidad de escribir código independiente de la máquina.
b) Mayor dificultad en la programación y en la comprensión de los programas.
c) El programador debe conocer más de un centenar de instrucciones.
Los ensambladores son por lo general más fáciles de programar que los
compiladores de lenguajes de alto nivel, y han estado disponibles desde la década
de los 50’s
Los ensambladores de alto nivel ofrecen posibilidades de abstracción que
incluyen:
Una de las principales ventajas del uso del ensamblador, es que se encarga de
administrar de manera transparente para el usuario la creación de memoria, las
bifurcaciones y el paso de parámetros entre funciones o procedimeinetos.
CS = Código
DS = Datos
SS = Pila
ES = E/S de puertos