Introducción a la programación de sistemas

Anuncio
INDICE
UNIDAD 1 .......................................................................................................................................... 5
Introducción a la programación de sistemas................................................................................ 5
1.1
¿Qué es y qué estudia la programación de sistemas? .............................................. 5
1.2
Herramientas desarrolladas con la teoría de programación de sistemas. .............. 6
1.3
Lenguajes. ......................................................................................................................... 8
1.3.1
Lenguajes naturales. ................................................................................................ 8
1.3.2
Lenguajes artificiales. .............................................................................................. 9
1.3.3
Proceso de la comunicación. .................................................................................. 9
1.4
Traductor y su estructura. ............................................................................................. 11
1.4.1
Ensambladores. ...................................................................................................... 11
1.4.2
Compiladores. ......................................................................................................... 12
1.4.3
Interpretes................................................................................................................ 14
1.5
Generadores de código para compiladores (compilador de compilador). ............. 14
UNIDAD 2 ........................................................................................................................................ 16
Introducción al diseño de los lenguajes de programación ....................................................... 16
2.1
Visión del problema........................................................................................................ 16
2.2
Consideraciones Preliminares. ..................................................................................... 18
2.3
Objetivos y filosofías del diseño de los lenguajes de programación. ..................... 19
2.4
Diseño detallado. ............................................................................................................ 20
2.5
Caso de estudio. ............................................................................................................. 20
UNIDAD 3 ........................................................................................................................................ 21
Análisis Léxico ................................................................................................................................ 21
3.1
Introducción a los Autómatas finitos y expresiones regulares. ............................... 21
3.2
Analizador de léxico. ...................................................................................................... 21
3.3
Manejo de localidades temporales de memoria (buffers). ....................................... 22
3.4
Creación de tablas de símbolos. .................................................................................. 23
3.5
Manejo de errores léxicos. ............................................................................................ 24
3.6
Generadores de código léxico: Lex y Flex. ................................................................ 24
UNIDAD 4 ........................................................................................................................................ 25
Análisis sintáctico ........................................................................................................................... 25
4.1
Introducción a las Gramáticas libres de contexto y árboles de derivación. .......... 25
4.2
Diagramas de sintaxis. ......................................................Error! Bookmark not defined.
4.3
Precedencia de operadores..............................................Error! Bookmark not defined.
4.4
Analizador sintáctico. .........................................................Error! Bookmark not defined.
4.4.1
Analizador descendente (LL). ...................................Error! Bookmark not defined.
4.4.2
Analizador ascendente (LR, LALR. .........................Error! Bookmark not defined.
4.5
Administración de tablas de símbolos.............................Error! Bookmark not defined.
4.6
Manejo de errores sintácticos y su recuperación. .........Error! Bookmark not defined.
4.7
Generadores de código para analizadores sintácticos: Yacc, Bison ................Error!
Bookmark not defined.
UNIDAD 5 ............................................................................................Error! Bookmark not defined.
Análisis semántico ..............................................................................Error! Bookmark not defined.
5.1
Analizador semántico.........................................................Error! Bookmark not defined.
5.2
Verificación de tipos en expresiones. ..............................Error! Bookmark not defined.
5.3
Conversión de tipos. ..........................................................Error! Bookmark not defined.
5.4
Acciones agregadas en un analizador sintáctico descendente (top-down). ....Error!
Bookmark not defined.
5.5
Pila semántica en un analizador sintáctico ascendente (bottom-up). ...............Error!
Bookmark not defined.
5.6
Administración de la tabla de símbolos. .........................Error! Bookmark not defined.
5.7
Manejo de errores semánticos. ........................................Error! Bookmark not defined.
UNIDAD 6 ............................................................................................Error! Bookmark not defined.
Generación de código intermedio ....................................................Error! Bookmark not defined.
6.1
Lenguajes intermedios.......................................................Error! Bookmark not defined.
6.2
Notaciones. ..........................................................................Error! Bookmark not defined.
6.2.1
Infija. .............................................................................Error! Bookmark not defined.
6.2.2
Postfija..........................................................................Error! Bookmark not defined.
6.2.3
Prefija. ..........................................................................Error! Bookmark not defined.
6.3
Representación de código intermedio.............................Error! Bookmark not defined.
6.3.1
Notación Polaca..........................................................Error! Bookmark not defined.
6.3.2
Codigo P. .....................................................................Error! Bookmark not defined.
6.3.3
Triplos. ..........................................................................Error! Bookmark not defined.
6.3.4
Cuádruplos. .................................................................Error! Bookmark not defined.
6.4
Esquemas de generación. ................................................Error! Bookmark not defined.
6.4.1
Expresiones. ................................................................Error! Bookmark not defined.
6.4.2
Declaración de variables, constantes......................Error! Bookmark not defined.
6.4.3
Estatuto de asignación. .............................................Error! Bookmark not defined.
6.4.4
Estatuto condicional. ..................................................Error! Bookmark not defined.
6.4.5
Estatuto de ciclos........................................................Error! Bookmark not defined.
6.4.6
Arreglos. .......................................................................Error! Bookmark not defined.
6.4.7
Funciones. ...................................................................Error! Bookmark not defined.
UNIDAD 7 ............................................................................................Error! Bookmark not defined.
Optimización........................................................................................Error! Bookmark not defined.
7.1
Tipos de optimización. .......................................................Error! Bookmark not defined.
7.1.1
Locales. ........................................................................Error! Bookmark not defined.
7.1.2
Bucles. ..........................................................................Error! Bookmark not defined.
7.1.3
Globales. ......................................................................Error! Bookmark not defined.
7.1.4
7.2
De mirilla. .....................................................................Error! Bookmark not defined.
Costos. .................................................................................Error! Bookmark not defined.
7.2.1
Costo de ejecución. ....................................................Error! Bookmark not defined.
7.2.2
Criterios para mejorar el código. ..............................Error! Bookmark not defined.
7.2.3
Herramientas para el análisis del flujo de datos. ...Error! Bookmark not defined.
UNIDAD 8 ............................................................................................Error! Bookmark not defined.
Generación de código objeto. ...........................................................Error! Bookmark not defined.
8.1
Lenguaje máquina. .............................................................Error! Bookmark not defined.
8.1.1
Características. ...........................................................Error! Bookmark not defined.
8.1.2
Direccionamiento. .......................................................Error! Bookmark not defined.
8.2
Lenguaje ensamblador. .....................................................Error! Bookmark not defined.
8.2.1
Características. ...........................................................Error! Bookmark not defined.
8.2.2
Almacenamiento. ........................................................Error! Bookmark not defined.
8.3
Registros. .............................................................................Error! Bookmark not defined.
8.3.1
Distribución. .................................................................Error! Bookmark not defined.
8.3.2
Asignación. ..................................................................Error! Bookmark not defined.
8.4
Administración de memoria. .............................................Error! Bookmark not defined.
UNIDAD 1
Introducción a la programación de sistemas
1.1 ¿Qué es y qué estudia la programación de sistemas?
¿QUÉ ES?
Un sistema es un conjunto de componentes que interaccionan entre si para
lograr un objetivo común. Las personas se comunican con el lenguaje, que es un
sistema muy desarrollado formado por palabras y símbolos que tienen significado
para el que habla y para quienes lo escuchan, lo mismo es para las computadoras
las cuales tienen sistemas y se comunican por medio de computadoras.
La programación es el proceso de convertir las especificaciones a grandes
rasgos de los sistemas en instrucciones de maquina que produzcan los resultados
deseados.
¿QUÉ ESTUDIA?
El trabajo de un programador de sistemas es seleccionar, modificar y mantener
el complejo software del sistema operativo. Por lo tanto, los programadores de
sistemas desempeñan una función de apoyo al mantener el ambiente del software
del sistema operativo en el que trabajan los programadores de aplicaciones y los
operadores de las computadoras. También participan en las decisiones relativas a
reducciones o ampliaciones de hardware y/o software. Programación de Sistemas
Conceptos y Aplicaciones Se entiende por programación de sistemas el conjunto
de programas necesario para que una computadora de una imagen coherente y
monolítica ante sus usuarios. Es un área especializada dentro de las ciencias de la
computación. Así, mediante la programación de sistemas, no solo se manejan las
computadoras por medio del lenguaje maquina (0 y 1) sino por otros sistemas
operativos, sin lo cual sería muy difícil la interacción con la maquina.
En esta área se estudia la teoría de máquinas y su aplicación en el diseño de
sistemas digitales y de arquitectura de computadoras. Áreas específicas: Sistemas
digitales para arquitecturas paralelas y control de procesos y sistemas
reconfigurables.
Inteligencia artificial aplicada a trabajo cooperativo En las aplicaciones
cooperativas realizadas en el entorno Web, es viable hacer uso de las
herramientas de inteligencia artificial. Se están diseñando y construyendo
herramientas para elaborar un sistema sin costura que opere en Web con la
finalidad de proporcionar a un grupo de coautores el soporte necesario para
producir conjunta y simultáneamente un mismo documento.
1.2 Herramientas desarrolladas con la teoría de programación de sistemas.
Las herramientas de programación, son aquellas que permiten realizar
aplicativos, programas, rutinas, utilitarios y sistemas para que la parte física del
computador u ordenador, funcione y pueda producir resultados.
Hoy en día existen múltiples herramientas de programación en el mercado,
tanto para analistas expertos como para analistas inexpertos.
Las herramientas de programación más comunes del mercado, cuentan hoy
día con programas de depuración o debugger, que son utilitarios que nos permiten
detectar los posibles errores en tiempo de ejecución o corrida de rutinas y
programas.
Muchas herramientas de software que manipulan programas fuente realizan
primero algún tipo de análisis. Algunos ejemplos de tales herramientas son:
 Editores de estructuras: Un editor de estructuras toma como entrada una
secuencia de órdenes para construir un programa fuente. El editor de estructuras
no sólo realiza las funciones de creación y modificación de textos de un editor de
textos ordinario, sino que también analiza el texto del programa, imponiendo al
programa fuente una estructura jerárquica apropiada. De esa manera, el editor de
estructuras puede realizar tareas adicionales útiles para la preparación de
programas. Por ejemplo, puede comprobar si la entrada está formada
correctamente, puede proporcionar palabras clave de manera automática (por
ejemplo, cuando el usuario escribe while, el editor proporciona el correspondiente
do y le recuerda al usuario que entre las dos palabras debe ir un condicional) y
puede saltar desde un begin o un paréntesis izquierdo hasta su correspondiente
end o paréntesis derecho. Además, la salida de tal editor suele ser similar a la
salida de la fase de análisis de un compilador.
 Impresoras estéticas: Una impresora estética analiza un programa y lo
imprime de forma que la estructura del programa resulte claramente visible. Por
ejemplo, los comentarios pueden aparecer con un tipo de letra especial, y las
proposiciones pueden aparecer con una indentación proporcional a la profundidad
de su anidamiento en la organización jerárquica de las proposiciones.
 Verificadores estáticos: Un verificador estático lee un programa, lo analiza e
intenta descubrir errores potenciales sin ejecutar el programa. La parte de análisis
a menudo es similar a la que se encuentra en los compiladores de optimización.
Así, un verificador estático puede detectar si hay partes de un programa que
nunca se podrán ejecutar o si cierta variable se usa antes de ser definida.
Además, puede detectar errores de lógica, como intentar utilizar una variable real
como apuntador, empleando las técnicas de verificación de tipos.
 Intérpretes: En lugar de producir un programa objeto como resultado de una
traducción, un intérprete realiza las operaciones que implica el programa fuente.
Para una proposición de asignación, por ejemplo, un intérprete podría construir un
árbol como el de la figura 1 y después efectuar las operaciones de los nodos
conforme “recorre” el árbol. En la raíz descubriría que tiene que realizar una
asignación, y llamaría a una rutina para evaluar la expresión de la derecha y
después almacenaría el valor resultante en la localidad de memoria asociada con
el identificador posición. En el hijo derecho de la raíz, la rutina descubriría que
tiene que calcular la suma de dos expresiones. Se llamaría a sí misma de manera
recursiva para calcular el valor de la expresión velocidad*60. Después sumaría
ese valor de la variable inicial. Muchas veces los intérpretes se usan para ejecutar
lenguajes de órdenes, pues cada operador que se ejecuta en un lenguaje de
órdenes suele ser una invocación de una rutina compleja, como un editor o un
compilador. Del mismo modo algunos lenguajes de “muy alto nivel”, normalmente
son interpretados, porque hay muchas cosas sobre los datos, como el tamaño y la
forma de las matrices, que no se pueden deducir en el momento de la
compilación.
 Compiladores: Tradicionalmente, se concibe un compilador como un
programa que traduce un programa fuente, como FORTRAN, al lenguaje
ensamblador o de máquina de algún computador. Sin embargo, hay lugares, al
parecer, no relacionados donde la tecnología de los compiladores se usa con
regularidad. La parte de análisis de cada uno de los siguientes ejemplos es
parecida a la de un compilador convencional.
 Formadores de textos. Un formador de textos toma como entrada una
cadena de caracteres, la mayor parte de la cual es texto para componer, pero
alguna incluye órdenes para indicar párrafos, figuras o estructuras matemáticas,
como subíndices o superíndices.
 Compiladores de circuitos de silicio. Un compilador de circuitos de silicio
tiene un lenguaje fuente similar o idéntico a un lenguaje de programación
convencional. Sin embargo las variables del lenguaje no representan localidades
de memoria, sino señales lógicas (0 o 1) o grupos de señales en un circuito de
conmutación. La salida es el diseño de un circuito en un lenguaje apropiado.
 Intérpretes de consultas. Un intérprete de consultas traduce un predicado
que contiene operadores relacionales y boléanos a órdenes para buscar en una
base de datos registros que satisfagan ese predicado.
1.3 Lenguajes.
Se llama lenguaje a cualquier tipo de código semiótico estructurado, para el
que existe un contexto de uso y ciertos principios combinatorios formales. Existen
muchos contextos tanto naturales como artificiales donde aparecen lenguajes.
El lenguaje humano se basa en la capacidad de los seres humanos para
comunicarse mediante de signos. Principalmente lo hacemos utilizando el signo
lingüístico. Aún así, hay diversos tipos de lenguaje. El lenguaje humano puede
estudiarse en cuanto a su desarrollo desde dos puntos de vista complementarios:
la ontogenia, que remite al proceso de adquisición del lenguaje por el ser humano,
y la filogenia El lenguaje animal se basa en el uso de señales sonoras, visuales y
olfativas a modo de signos para referirse a un referente o un significado diferente
de dichas señales. Dentro del lenguaje animal están los gritos de alarma, el
lenguaje de las abejas, etc. Los lenguajes formales son construcciones artificiales
humanas, que se usan en matemática y otras disciplinas formales, incluyendo
lenguajes de programación. Estas construcciones tienen estructuras internas que
comparten con el lenguaje humano natural, por lo que pueden ser en parte
analizados con los mismos conceptos que éste.
1.3.1
Lenguajes naturales.
Este tipo de lenguaje es el que nos permite el designar las cosas actuales y
razonar a cerca de ellas, fue desarrollado y organizado a partir de la experiencia
humana y puede ser utilizado para analizar situaciones altamente complejas y
razonar muy sutilmente. La riqueza de sus componentes semánticos da a los
lenguajes naturales su gran poder expresivo y su valor como una herramienta para
razonamiento sutil. Por otro lado la sintaxis de un LN puede ser modelada
fácilmente por un lenguaje formal, similar a los utilizados en las matemáticas y la
lógica. Otra propiedad de los lenguajes naturales es la polisemántica, es decir la
posibilidad de que una palabra en una oración tenga diversos significados.
En un primer resumen, los lenguajes naturales se caracterizan por las
siguientes propiedades:
 Desarrollados por enriquecimiento progresivo antes de cualquier intento de
formación de una teoría.
 La importancia de su carácter expresivo debido grandemente a la riqueza del
componente semántico (polisemántica).
 Dificultad o imposibilidad de una formalización completa.
1.3.2 Lenguajes artificiales.
El lenguaje nos permite hacer la abstracción y conceptualización de ideas y por
medio de este comunicarnos. En las distintas ramas de la ciencia nos
encontramos con lenguajes artificiales o mejor conocidos como lenguajes formales
que limitan su alcance a su materia de estudio.
Concretamente y después de esta breve introducción, en la actualidad nos
encontramos con distintos lenguajes para programar y también los campos de
aplicación son variados y completamente distintos. De acuerdo a la complejidad
del problema a resolver será necesario hacer una selección adecuada del lenguaje
de programación (incluso se puede generar el lenguaje de programación propio)
que permita resolver de manera eficiente el problema.
Hasta hace pocos años, el software de desarrollo se basaba
fundamentalmente en Lenguajes Artificiales (Basic, C, Cobol, Pascal…) para
“explicar” a la computadora las acciones a realizar. Por el contrario, los
“Orientados a Objetos” permiten que el programador ignore el lenguaje
comprensible para la máquina poniendo a su disposición una serie de objetos
preprogramados. De este modo la tarea se simplifica enormemente y se reduce a
disponer dentro de la ventana de programa y en la secuencia adecuada, los
programas y funciones de los que nos provee.
1.3.3 Proceso de la comunicación.
La comunicación es un fenómeno inherente a la relación grupal de los seres
vivos por medio del cual éstos obtienen información acerca de su entorno y de
otros entornos y son capaces de compartirla haciendo partícipes a otros de esa
información. La comunicación es de suma importancia para la supervivencia de
especies gregarias, pues la información que ésta extrae de su medio ambiente y
su facultad de transmitir mensajes serán claves para sacar ventaja del modo de
vida gregario.
Etimológicamente, la palabra comunicación deriva del latín “communicare”, que
puede traducirse como “poner en común, compartir algo”. Se considera una
categoría polisémica en tanto su utilización no es exclusiva de una ciencia social
en particular, teniendo connotaciones propias de la ciencia social de que se trate.
Proceso de transmisión de información de un emisor (A) a un receptor (B) a
través de un medio (c). En la transmisión y la recepción de esa información se
utiliza un código específico que debe ser “codificado”, por el emisor y
“decodificado” por el receptor”.
Elementos del Proceso de la comunicación:
Los elementos de la comunicación humana son: fuente, emisor o codificador,
código (reglas del signo, símbolo), mensaje primario (bajo un código), receptor o
decodificador, canal, ruido (barreras o interferencias) y la retroalimentación o
realimentación (feed-back, mensaje de retorno o mensaje secundario).
• Fuente: Es el lugar de donde emana la información, los datos, el contenido
que se enviará, en conclusión: de donde nace el mensaje primario.
• Emisor o codificador: Es el punto (persona, organización…) que elige y
selecciona los signos adecuados para transmitir su mensaje; es decir, los codifica
para poder llevarlo de manera entendible al receptor. En el emisor se inicia el
proceso comunicativo.
• Receptor o decodificador: Es el punto (persona, organización…) al que se
destina el mensaje, realiza un proceso inverso al del emisor ya que en él está el
descifrar e interpretar lo que el emisor quiere dar a conocer. Existen dos tipos de
receptor, el pasivo que es el que sólo recibe el mensaje, y el receptor activo o
perceptor ya que es la persona que no sólo recibe el mensaje sino que lo percibe y
lo almacena. El mensaje es recibido tal como el emisor quiso decir, en este tipo de
receptor se realiza lo que comúnmente denominamos el feed-back o
retroalimentación.
• Código: Es el conjunto de reglas propias de cada sistema de signos y
símbolos que el emisor utilizará para trasmitir su mensaje, para combinarlos de
manera arbitraria porque tiene que estar de una manera adecuada para que el
receptor pueda captarlo. Un ejemplo claro es el código que utilizan los marinos
para poder comunicarse; la gramática de algún idioma; los algoritmos en la
informática…, todo lo que nos rodea son códigos.
• Mensaje: Es el contenido de la información (contenido enviado): el conjunto
de ideas, sentimientos, acontecimientos expresados por el emisor y que desea
trasmitir al receptor para que sean captados de la manera que desea el emisor. El
mensaje es la información.
• Canal: Es el medio a través del cual se transmite la informacióncomunicación, estableciendo una conexión entre el emisor y el receptor. Mejor
conocido como el soporte material o espacial por el que circula el mensaje.
Ejemplos: el aire, en el caso de la voz; el hilo telefónico, en el caso de una
conversación telefónica.
• Referente: Realidad que es percibida gracias al mensaje. Comprende todo
aquello que es descrito por el mensaje.
• Situación: Es el tiempo y el lugar en que se realiza el acto comunicativo.
• Interferencia o barrera: Cualquier perturbación que sufre la señal en el
proceso comunicativo, se puede dar en cualquiera de sus elementos. Son las
distorsiones del sonido en la conversación, o la distorsión de la imagen de la
televisión, la alteración de la escritura en un viaje, la afonía del hablante, la
sordera del oyente, la ortografía defectuosa, la distracción del receptor, el alumno
que no atiende aunque esté en silencio.
• Retroalimentación o realimentación (mensaje de retorno): Es la condición
necesaria para la interactividad del proceso comunicativo, siempre y cuando se
reciba una respuesta (actitud, conducta…) sea deseada o no. Logrando la
interacción entre el emisor y el receptor. Puede ser positiva (cuando fomenta la
comunicación) o negativa (cuando se busca cambiar el tema o terminar la
comunicación). Si no hay realimentación, entonces solo hay información más no
comunicación.
1.4 Traductor y su estructura.
En un sentido orientado hacia la computación, un traductor, de manera
general, es un software que toma como entrada un programa escrito en un código
llamado fuente y genera como salida otro programa en un código llamado objeto.
Algunos ejemplos de traductores son los compiladores (toma como entrada
código en alto nivel y genera como salida código en bajo nivel), los interpretes
(toma como entrada código en alto nivel y genera como salida un código
intermedio), los preprocesadores (toma como entrada código en alto nivel y
genera como salida código en alto nivel) y el ensamblador (toma como entrada
código en ensamblador y genera como salida código en bajo nivel).
Su estructura podria ser expresada de la siguiente manera:
código fuente -→>> traductor -→> código objeto
1.4.1 Ensambladores.
El término ensamblador (del inglés assembler) se refiere a un tipo de programa
informático que se encarga de traducir un fichero fuente escrito en un lenguaje
ensamblador, a un fichero objeto que contiene código máquina, ejecutable
directamente por la máquina para la que se ha generado. El propósito para el que
se crearon este tipo de aplicaciones es la de facilitar la escritura de programas, ya
que escribir directamente en código binario, que es el único código entendible por
la computadora, es en la práctica imposible. La evolución de los lenguajes de
programación a partir del lenguaje ensamblador originó también la evolución de
este programa ensamblador hacia lo que se conoce como programa compilador.
Funcionamiento:
El programa lee el fichero escrito en lenguaje ensamblador y sustituye cada
uno de los códigos mnemotécnicos que aparecen por su código de operación
correspondiente en sistema binario.
Tipos de ensambladores:
Podemos distinguir entre tres tipos de ensambladores:
• Ensambladores básicos. Son de muy bajo nivel, y su tarea consiste
básicamente en ofrecer nombres simbólicos a las distintas instrucciones,
parámetros y cosas tales como los modos de direccionamiento. Además, reconoce
una serie de directivas (o meta instrucciones) que indican ciertos parámetros de
funcionamiento del ensamblador.
• Ensambladores modulares, o macro ensambladores. Descendientes de los
ensambladores básicos, fueron muy populares en las décadas de los 50 y los 60,
antes de la generalización de los lenguajes de alto nivel. Hacen todo lo que puede
hacer un ensamblador, y además proporcionan una serie de directivas para definir
e invocar macroinstrucciones (o simplemente, macros). Véase X86.
• Ensambladores modulares 32-bits o de alto nivel. Son ensambladores que
aparecieron como respuesta a una nueva arquitectura de procesadores de 32 bits,
muchos de ellos teniendo compatibilidad hacia atrás pudiendo trabajar con
programas con estructuras de 16 bits. Además de realizar la misma tarea que los
anteriores, permitiendo también el uso de macros, permiten utilizar estructuras de
programación más complejas propias de los lenguajes de alto nivel.
1.4.2 Compiladores.
Un compilador acepta programas escritos en un lenguaje de alto nivel y los
traduce a otro lenguaje, generando un programa equivalente independiente, que
puede ejecutarse tantas veces como se quiera. Este proceso de traducción se
conoce como compilación.
• El de los programas de partida (LA)
• El de los programas equivalentes traducidos (LB), normalmente el lenguaje
de máquina
• El lenguaje en que está escrito el propio compilador (LC), que puede ser igual
o diferente a LA. Aumenta la portabilidad del compilador si está escrito en el
mismo lenguaje, es decir, se puede compilar a sí mismo.
Los programas interpretados suelen ser más lentos que los compilados, pero
los intérpretes son más flexibles como entornos de programación y depuración.
Partes de un compilador:
Normalmente los compiladores están divididos en dos partes:
• Front End: es la parte que analiza el código fuente, comprueba su validez,
genera el árbol de derivación y rellena los valores de la tabla de símbolos. Esta
parte suele ser independiente de la plataforma o sistema para el cual se vaya a
compilar.
• Back End: es la parte que genera el código máquina, específico de una
plataforma, a partir de los resultados de la fase de análisis, realizada por el Front
End.
Tipos de compiladores:
Esta taxonomía de los tipos de compiladores no es excluyente, por lo que
puede haber compiladores que se adscriban a varias categorías:
• Compiladores cruzados: generan código para un sistema distinto del que
están funcionando.
• Compiladores optimizadores: realizan cambios en el código para mejorar su
eficiencia, pero manteniendo la funcionalidad del programa original.
• Compiladores de una sola pasada: generan el código máquina a partir de una
única lectura del código fuente.
• Compiladores de varias pasadas: necesitan leer el código fuente varias veces
antes de poder producir el código máquina.
• Compiladores JIT (Just In Time): forman parte de un intérprete y compilan
partes del código según se necesitan.
Pauta de creación de un compilador: En las primeras épocas de la informática,
el software de los compiladores era considerado como uno de los más complejos
existentes.
Los primeros compiladores se realizaron programándolos directamente en
lenguaje máquina o en ensamblador. Una vez que se dispone de un compilador,
se pueden escribir nuevas versiones del compilador (u otros compiladores
distintos) en el lenguaje que compila ese compilador.
Actualmente existen herramientas que facilitan la tarea de escribir
compiladores ó intérpretes informáticos. Estas herramientas permiten generar el
esqueleto del analizador sintáctico a partir de una definición formal del lenguaje de
partida, especificada normalmente mediante una gramática formal y barata,
dejando únicamente al programador del compilador la tarea de programar las
acciones semánticas asociadas.
1.4.3 Interpretes.
Un intérprete es un traductor que toma un programa fuente, lo traduce y a
continuación lo ejecuta. BASIC es un lenguaje interpretado.
Se trata de traductores-ejecutores ya que con cada instrucción realizan un
proceso triple de lectura-traducción-ejecución. Son relativamente lentos, pero muy
buenos para la depuración de programas.
Se puede también utilizar una alternativa diferente de los compiladores para
traducir lenguajes de alto nivel. En vez de traducir el programa fuente y grabar en
forma permanente el código objeto que se produce durante la corrida de
compilación para utilizarlo en una corrida de producción futura, el programador
sólo carga el programa fuente en la computadora junto con los datos que se van a
procesar. A continuación, un programa intérprete, almacenado en el sistema
operativo del disco, o incluido de manera permanente dentro de la máquina,
convierte cada proposición del programa fuente en lenguaje de máquina conforme
vaya siendo necesario durante el proceso de los datos. No se graba el código
objeto para utilizarlo posteriormente.
La siguiente vez que se utilice una instrucción, se le debe interpretar otra vez y
traducir a lenguaje máquina. Por ejemplo, durante el procesamiento repetitivo de
los pasos de un ciclo, cada instrucción del ciclo tendrá que volver a ser
interpretado cada vez que se ejecute el ciclo, lo cual hace que el programa sea
más lento en tiempo de ejecución (porque se va revisando el código en tiempo de
ejecución) pero más rápido en tiempo de diseño (porque no se tiene que estar
compilando a cada momento el código completo). El intérprete elimina la
necesidad de realizar una corrida de compilación después de cada modificación
del programa cuando se quiere agregar funciones o corregir errores; pero es obvio
que un programa objeto compilado con antelación deberá ejecutarse con mucha
mayor rapidez que uno que se debe interpretar a cada paso durante una corrida
de producción.
1.5 Generadores de código para compiladores (compilador de compilador).
Aquí se hablará de las herramientas generadoras automáticas de código para
un compilador. Estas herramientas trabajan basadas en un conjunto de reglas;
estas reglas definen la traducción de las instrucciones del lenguaje intermedio al
lenguaje de máquina.
Para la generación de código, se busca en las reglas establecidas la
proposición que coincida con la entrada actual; la entrada actual proviene de un
árbol. Un ejemplo de esto seria
Entonces el compilador recibe una entrada de caracteres, por lo general escrita
por el programador; el compilador realiza los análisis: léxico, sintáctico y
semántico, para generar seguidamente el código intermedio, el código intermedio
se genera con principios de búsqueda de patrones y aplicación de reglas.
Después se hace la optimización del código intermedio; seguidamente se realiza
la generación de código objeto en lenguaje de máquina.
En síntesis para crear un generador de código se deben hacer muchas de las
tareas que realizan los compiladores; algunas de estas tareas son: la búsqueda de
patrones, la escritura de código, el análisis sintáctico, el análisis léxico y la
optimización de código. Estas tareas las realiza el desarrollador una vez para una
arquitectura específica.
METACOMPILADOR: Es Sinónimo De compilador de compiladores y se refiere
a un programa que recibe como entrada las especificaciones del lenguaje para el
que se desea obtener un compilador y genera como salida el compilador para ese
lenguaje. El desarrollo de los metacompiladores se encuentra con la dificultad de
unir la generación de código con la parte de análisis. Lo que sí se han desarrollado
es generadores de analizadores léxicos y sintácticos.
Por ejemplo, los conocidos:
LEX: generador de analizadores léxicos
YACC: analizadores sintácticos desarrollados para• generador de UNIX.
Los inconvenientes que tienen son que los analizadores que generan no son
muy eficientes.
UNIDAD 2
Introducción al diseño de los lenguajes de programación
2.1 Visión del problema.
Proporciona modelos de diseño que permitan caracterizar el desarrollo de
aplicaciones utilizando un lenguaje de programación se necesita un lenguaje de
modelado que sea capaz de capturar la semántica del modelo al que se ajusta el
lenguaje de programación.
El modelado de diseño proporcionado por el lenguaje de modelado debe ser
capas de capturar la semántica del programa que implementa la especificación de
requisitos.
En los últimos años, una de las artes más predominantes en el mundo de la
programación ha sido el diseño de lenguaje de programación. El numero de
lenguajes de programación propuesta y diseñados son extremadamente grandes.
Aun el numero de de lenguajes para el que un compilador ha aplicado es inmenso.
Sammet (1976) indica 167 en su lista 1974–1975. Aun que los primeros lenguajes
de programación primitivos nacieran cerca 25 años atrás, hasta que reciente
mente hubiesen un pequeño proceso en el diseño de nuevos lenguajes de
programación.
Los primeros lenguajes fueron los pioneros, explorando un nuevo campo. No
es de sorprenderse que carecieran de un buen diseño. No se debería criticar a los
diseñadores o FORTRAN; puesto que suficientes problemas tenían con diseñar y
aplicar uno de los primeros lenguajes de alto nivel Si hay cualquier crítica de ser
concedida con respecto a FORTRAN, Nadie razonablemente los podría esperar
que a la crítica sean concedidos con respecto a 25 años más tarde, sus objetivos
deben ser los usuarios que se han adherido tan tenazmente a ciertos diseñadores
caídos en desuso del lenguaje que tienen tan perpetuaron con entusiasmo los
desperfectos de FORTRAN. Se debe notar que nuestras referencias a FORTRAN
en el párrafo anterior y a través de este capítulo se refiere a FORTRAN IV antes
que FORTRAN 77.
Después que el desarrollo inicial del lenguaje de alto nivel y la implementación
de los primeros pocos compiladores, allí resultó un período bastante largo en el
que las tentativas conscientes se hicieron para diseñar nuevos lenguajes sin los
desperfectos de los viejos. La mayor parte de estas tentativas eran los fracasos,
no tanto de una falta de ideas en cómo diseñar mejores lenguajes como de un
superávit de ideas. Una buena ampliación de este proceso es la noción que “si
podría significar algo, debería” (Radin y Rogoway, 1965), que llevó a PL/YO. Más
recientemente, la experiencia de errores pasados había llevado al conocimiento
verdadero acerca de cómo construir mejores lenguajes de programación.
Las ideas y los principios básicos se establecen suficientemente bien para
indicar las pautas explícitas para el diseño del lenguaje. Esas áreas que aun no
han sido comprendidas se encuentran bajo investigación.
Esta discusión procurará por consiguiente acentuar un sistema. El enfoque
ordenado al diseño del lenguaje, se debe recordar, sin embargo, hacer la
justificación apropiada a muchos temas que a menudo son necesarios para
discutir detalles así como generalidades. El campo del diseño del lenguaje no es
de ninguna manera completamente desarrollada, y muchas áreas no han sido bien
unificadas. También, muchas áreas interrelacionan y son difícil de discutirlos
separadamente.
Por la necesidad, esta discusión sin embargo, restringe su alcance. Las
descripciones elaboradas de las características posibles del lenguaje se limitarán
se asume que el diseñador potencial del lenguaje tiene las bases suficientes en
lenguajes de programación para estar enterado de las ideas prinsipales. Las
características específicas se discutirán para especificar razones , pero ninguna
tentativa se hará para dar un catálogo general. Hay ya varios catálogos , como:
(Elson, 1973; Pratt, 1975; Nlcholls, 1975). Una proposición básica de este capítulo
entero es que un buen lenguaje no es apenas una colección casual de
características de un total unificado. Se asumirá que los lenguajes, bajo la
discusión son” de alto nivel” los idiomas. La discusión será restringida también en
gran parte a idiomas procesales para escribir software (“software” se utiliza aquí
en sus la mayoría de los sentidos generales para significar “los programas para
ser utilizados por otra persona”). Mucho de lo que se dice será aplicable a otras
clases de lenguajes.
El diseñar completamente un lenguaje. Si es que es el enfoque se toma, como
sea, se debe tomar con cuidado para no hacer una extensión tan grande y
compleja como se llega a ser, el hecho, de un nuevo lenguaje. En tales casos, la
necesidad de retener algunas interfaces con un viejo lenguaje probablemente
cederá gravemente el diseño de la extensión. También, si uno extiende un
lenguaje existente, es necesario escoger cuidadosamente un lenguaje base para
que el trabajo de la extensión se aminorare y la extensión elegantemente quede
dentro del lenguaje. El objetivo debe ser el de producir un lenguaje el cual se más
grande aun que igualmente bien construido.
¿Sería posible el modificar un lenguaje existente, utilizando posiblemente un
macroprocessor o algo similar? Aun que con facilidad un macro contrario de
menor parámetro (sustituyendo simplemente un texto especificado para cada
ocurrencia de una identificación definida) podría producir modificaciones mayores
en la sintaxis de un lenguaje, si se utilizara diestramente (por ejemplo, RATFOR
definido por Kernighan y Plauger, 19746) sin embargo, el poder de este enfoque
para una tarea más compleja, tal como la introducción de nuevas estructuras de
datos, se limitan.
Algunas consideraciones serias deben ser dadas a estas técnicas como
alternativas para un nuevo lenguaje, con el simple motivo de aminorar el trabajo y
el tiempo implicado. Quizás no haya ningún otro problema relacionado con la
computadora que observe tan tentadoramente fácil y sea sumamente terrible un
buen trabajo de diseño de lenguaje. Prescinda de la noción que es posible agitar
un diseño el fin de semana y el comenzar aplicando un traductor para el lunes. Un
mes luego habrá de asentarse todavía los puntos secundarios del diseño del
idioma y la implementación no habrá obtenido casi en ningún lugar.
2.2 Consideraciones Preliminares.
Debemos tomar en cuenta las palabras reservadas del lenguaje, los
operadores, los tipos de datos.
Debemos considerar el objetivo del lenguaje, si es un lenguaje de enseñanza,
si es un lenguaje para profesionales, si el código desarrollado va a ser mejor.
Los factores fundamentales en la calidad del software son:
 La eficiencia: capacidad para el aprovechamiento óptimo de los recursos que
emplea.
 La portabilidad: facilidad para ser ejecutados en distintos entornos lógicos o
físicos.
 La verificabilidad: capacidad para soportar procedimientos de pruebas, test o
ensayos.
 La integridad: nivel de protección frente a procesos que traten de alterarlo.
 La facilidad de uso: comodidad y claridad en la interacción con el usuario.
 La exactitud: nivel de precisión que alcanzan los resultados obtenidos.
 La robustez: capacidad para funcionar correctamente en situaciones extremas.
 La extensibilidad: capacidad para adaptar su funcionamiento al incremento en
sus objetivos.
 La compatibilidad: facilidad de poder ser aplicados en conjunción con otros
programas.
 La reutilización: posibilidad de utilizarlos (total o parcialmente) en nuevos
contextos.
2.3 Objetivos y filosofías del diseño de los lenguajes de programación.
Objetivo:
El principal objetivo del lenguaje es, por supuesto, servir de apoyo didáctico en
una materia de lenguajes de programación, intérpretes y compiladores. Derivado
de la persecución de este objetivo surgen varias metas específicas y objetivos
particulares:
• La apreciación del desarrollo e implementación de un lenguaje de
programación.
• La comprensión del procedimiento seguido en la formación de un conjunto de
reglas gramaticales que permiten identificar y nombrar sin ambigüedad acciones y
secuencias ordenadas de acciones sobre el contexto específico de un problema
en particular.
• Proporcionar un medio de familiarización con la realización de operaciones
aritméticas usando una pila y su posterior extensión para la manipulación de otros
datos.
• La clara especificación y adecuada documentación del proceso de creación o
extensión de un lenguaje y sus resultados.
Filosofía:
El lenguaje de programación esta pensado para la programación evolutiva.
Esta consiste en un método de programación basado en un ciclo de prueba y error
donde se refina un programa hasta conseguir que haga lo que queremos. Esta
forma de programar se aplica a problemas donde se desconoce que algoritmo nos
llevará a la solución. Esta situación se da en investigación y en la creación de
prototipos donde hay que realizar muchas pruebas hasta dar con la solución más
apropiada. Para estos casos, es más apropiado el uso de un interprete que un
compilador, ya que de esta forma se reduce el tiempo invertido en cada prueba.
Para que un lenguaje sea efectivo en programación evolutiva tiene que facilitar:
la interacción, la modificación del programa y aportar instrucciones de alto nivel
cercanas al problema. Estos tres puntos se consiguen cuando el lenguaje tiene las
siguientes características:
Estado de Interacción. Entre prueba y prueba es interesante guardar el estado
de ejecución. De esta forma se evita repetir la ejecución de las instrucciones
necesarias para llegar al estado de ejecución donde queremos realizar pruebas.
Los programas implementa esta característica mediante un ámbito global dinámico
que guarda funciones y variables mientras se utiliza el intérprete.
Sintaxis Cercana al Problema. Es más efectivo escribir en una notación
cercana al problema que adaptarse a la sintaxis de un lenguaje de programación.
De esta forma se evita el paso de traducción que tiene que realizar el programador
antes de escribir una nueva sentencia del programa.
2.4 Diseño detallado.
El diseño detallado tiene que ver con la especificación de detalles algorítmicos,
representaciones concretas de datos, interconexiones entre funciones y
estructuras de datos, y empaque del producto de programación. El diseño
detallado está fuertemente influenciado por el lenguaje de instrumentación, pero
no es lo mismo que la instrumentación; el diseño detallado tiene que ver más con
aspectos semánticos y menos con detalles sintácticos que es la instrumentación.
El punto de inicio para el diseño detallado es una estructura arquitectónica a la
que se le van a proporcionar los detalles algorítmicos y las representaciones
concretas de datos. Mientras que hay una fuerte tentación para proceder
directamente de la estructura arquitectónica a la instrumentación, hay varias
ventajas que pueden lograrse en el nivel intermedio de detalle proporcionado por
el diseño detallado.
La instrumentación comunica los aspectos de la sintaxis del lenguaje de
programación, el estilo de codificación la documentación interna, y la inserción de
pruebas y depuraciones al código. Las dificultades que se encuentran durante la
instrumentación casi siempre se deben al hecho de que el instrumentador
simultáneamente está realizando análisis, diseño y actividades de codificación
mientras intenta expresar el resultado final en un lenguaje de instrumentación. El
diseño detallado permite el diseño de algoritmos y representaciones de datos en
un nivel más alto de abstracción y notación que el que proporciona el lenguaje de
instrumentación.
El diseño detallado separa la actividad de diseño a bajo nivel de la
instrumentación, igual que las actividades de análisis y diseño aíslan las
consideraciones de lo que se desea de la estructura que logrará los resultados
deseados. Una especificación adecuada de diseño detallado minimiza el número
de sorpresas durante la instrumentación del producto.
2.5 Caso de estudio.
UNIDAD 3
Análisis Léxico
3.1 Introducción a los Autómatas finitos y expresiones regulares.
Autómata:
Un autómata finito o máquina de estado finito es un modelo matemático de un
sistema que recibe una cadena constituida por símbolos de un alfabeto y
determina si esa cadena pertenece al lenguaje que el autómata reconoce.
Definición formal
Formalmente, un autómata finito (AF) puede ser descrito como una 5-tupla
(S,Σ,T,s,A) donde:
* S un conjunto de estados;
* Σ es un alfabeto;
* T es la función de transición:
* s es el estado inicial;
* A es un conjunto de estados de aceptación o finales.
3.2 Analizador de léxico.
El analizador léxico es la primera fase de un compilador, lee caracteres de
entrada para formar componentes e identificarlos o clasificarlos y pasar la
información de los componentes al analizador sintáctico.
Realiza además funciones como eliminar espacios en blanco, saltos de línea,
tabuladores, ignorar comentarios, detección y recuperación de errores. Los errores
que un analizador léxico reconoce son símbolos no válidos o no reconocidos por el
léxico del lenguaje o que no forman parte de ningún componente léxico.
Existen diversos métodos para construir una analizador léxico, un método es
usar un AFD para describir el patrón de cada componente. Para este método se
parte de una tabla descriptiva de los componentes léxicos que reconocerá el
analizador, en donde se clasifican los diversos tipos de componentes.
Para cada componente se construye un AFD que permita identificar el patrón
de símbolos que deberá recibir y donde deberá terminar ese patrón que señale
que ahí se encontró un componente. Todos los AFD´s se integran en un solo
diagrama. Finalmente se determina la matriz de transición de estados, que no es
más que transcripción del AFD integrado, en donde las columnas son los símbolos
que acepta el analizador, incluyendo una columna para \b \t \n y otra columna para
“otro” que representa a cualquier caractér o símbolo diferente de los señalados por
el analizador. Lo que resta es crear un algoritmo de reconocimiento de cadenas y
añadir mediante una selección de casos para tomar la acción correspondiente
según la cadena encontrada.
3.3 Manejo de localidades temporales de memoria (buffers).
La forma más fácil de leer un programa es carácter por carácter pero es
ineficiente.
La forma más eficiente es realizar una copia a la memoria de todo el código
fuente. Pero esto en la gran mayoría de las ocasiones es impráctico por las
dimensiones de los programas. Para solucionar este problema se sugiere utilizar
buffers.
Manejo de buffers:
Existen muchas formas de dividir el trabajo, pero siempre se deberá llevar dos
punteros, uno al carácter actual y otro al inicial del lexema.
El manejo de buffers es esencial para realizar el análisis de grandes programas
de mejor manera
La diferente de velocidad entre los dos tipos de memoria es muy grande, por lo
que resulta interesante definir algún tipo de estrategia que reduzca este
diferencial.
Un buffer se define como un conjunto de bytes que son leídos o escritos desde
un dispositivo de almacenamiento, en la memoria primaria. Cuando se desea leer
una información, se lee un bloque de información en el que aparece. La
modificación de un dato se realiza sobre el buffer, que posteriormente debe ser
enviado al dispositivo de almacenamiento. La utilización de esta técnica permite
reducir el número de accesos a memoria secundaria.
Número de Buffers y Velocidad de Acceso:
El manejo de buffers por parte del administrador de ficheros permite reducir el
número de accesos a memoria secundaria. Pero una cuestión fundamental es el
número de buffers a utilizar. Si sólo se utiliza un buffer, un problema que realice
lecturas y escrituras de modo alterno, debería leer un bloque en cada operación. Esto se resuelve mediante la utilización de un buffer para escritura y otro para
lectura. Pero la lectura, o escritura, alterna sobre varios ficheros puede provocar el
mismo problema. Otra alternativa es la utilización de ambos bloques para lecturas
y escrituras de modo alternado.
La generalización de esta idea es el caso real, varios buffers que se manejan
de modo indistinto para lecturas y escrituras. La gestión de estos buffers es
realizada por el administrador de ficheros, aunque el usuario puede controlar el
número de buffers.
Si todos los buffers están ocupados, se debe vaciar uno de ellos para
posibilitar una lectura. Normalmente se utiliza al algoritmo LRU, es decir, se vacía
el buffer menos recientemente utilizado.
3.4 Creación de tablas de símbolos.
Tabla: conjunto de pares clave-valor, llamados elementos de la tabla.
La tabla de símbolos es una componente necesaria de un compilador. Al
declarar un identificador (normalmente una sola vez), éste es insertado en la tabla.
Cada vez que se utilice el identificador se realizará una búsqueda en la tabla para
obtener la información asociada (el valor).





Búsqueda: dada la clave de un elemento, encontrar su valor.
Inserción: Dado un par clave-valor, añadir un elemento nuevo a la tabla.
Cambio de valor: Buscar el elemento y cambiar su valor.
Borrado: Eliminar un elemento de la tabla.
Longitud de búsqueda (o tiempo de acceso):
De una clave: Li = número de comparaciones con elementos de la tabla para
encontrar esa clave. Máxima: LM = número máximo de comparaciones para
encontrar cualquier clave. Media (esperada): Lm = número medio de
comparaciones para encontrar un valor. Si la frecuencia de todas las claves es la
misma:
Lm = (S Li)/N
Si la frecuencia de todas las claves no es la misma:
Lm = S pi.Li
Grado de ocupación:
s = n/N
donde n=número de elementos en la tabla y N=capacidad máxima de la tabla.
Función de búsqueda: B : K→E asocia a cada clave k un elemento B(k).
Valor asociado a una clave k: v(B(k)). Puede ser múltiple, en cuyo caso
normalmente se convierte en un puntero. Si está en la tabla puede almacenarse
consecutivamente o en subtablas paralelas.
Tablas de símbolos (identificadores) La clave es el identificador. El valor está
formado por:
Atributos del identificador. Puntero a la posición de memoria asignada. La clave
puede sustituirse por un puntero.
Los identificadores pueden estar empaquetados.
La longitud del identificador puede especificarse en la tabla o delante del
nombre, o ser implícita.
Tablas consecutivas: Todos los elementos ocupan posiciones de memoria
adyacentes. Tablas ligadas: cada elemento apunta al siguiente. Tablas
doblemente ligadas: cada elemento apunta al siguiente y al anterior. Tablas no
ordenadas Inserción: en el primer lugar vacío.
3.5 Manejo de errores léxicos.
Recuperación en modo pánico: este tipo de estrategia es la más común.
Consiste en que cuando se detecta una cadena no reconocible, se siguen leyendo
caracteres hasta que se vuelve a detectar un token válido. Borrar un carácter
extraño. Insertar un carácter que falta (e.g. reemplazar 2C por 2*C). Reemplazar
un carácter incorrecto por otro correcto (e.g. reemplazar INTEJER por INTEGER si
el lugar en donde aparece el primer lexema no es el indicado para un identificador)
Intercambiar dos caracteres, ó tokens, adyacentes (e.g. I INTEGER por INTEGER
I).
La recuperación de errores durante el AL puede producir otros en las
siguientes fases. var numero : integer; begin num?ero:=10; end el compilador
podría producir los siguientes mensajes de error: ERROR LÉXICO: carácter no
reconocido (?) ERROR SEMÁNTICO: identificador no declarado (num) ERROR
SINTÁCTICO: falta operador entre identificadores ERROR SEMÁNTICO:
identificador no declarado (ero)
Otras veces no: var i,j: integer; begin i:=1; ? j:=2; end
3.6 Generadores de código léxico: Lex y Flex.
UNIDAD 4
Análisis sintáctico
4.1 Introducción a las Gramáticas libres de contexto y árboles de derivación.
Gramáticas de Contexto Libre:
La flexibilidad proporcionada por las gramáticas de contexto libre es tal que es
la más usada para definir la sintaxis de los lenguajes de programación.
Una definición formal de una gramática de contexto sensitivo es la siguiente:
Es un cuádruplo G= (V, S , P, S) donde V es un conjunto finito de variables, S
es un conjunto finito de símbolos terminales, P es un conjunto finito de reglas y S
es el símbolo inicial.
Cada producción tiene la forma uVv, donde u es una variable del conjunto V, y
v es un miembro de (V È S)* . Esto quiere decir En la parte izquierda dela
producción viene siempre una variable (símbolo no terminal) y en la parte derecha
pueden venir cualquier número de símbolos terminales y no terminales incluyendo
la cadena nula.
Una gramática de contexto libre produce un lenguaje también de contexto libre:
G à L(G).
Arboles De Derivación:
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.
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.
4.2 Diagramas de sintaxis.
Un segundo método alternativo para desplegar las producciones de ciertas
gramáticas de tipo 2 es el diagrama de sintaxis. Ésta es una imagen de la
producciones que permite al usuario ver las sustituciones en forma dinámica, es
decir, verlas como un movimiento a través del diagrama. En la figura 10.5 se
ilustrará los diagramas que resultan de la traducción de conjuntos de producciones
típicos, que son, por lo general, todas las producciones que aparecen en el lado
derecho de algún enunciado BNF.
a) <w> ::= <w1> <w2> <w3>
b) ‹w> ::= <w1><w2> | ‹w1›a | bc<w2›
c) <w> ::= ab<w>.
d) ‹w> ::= ab | ab<w›.
4.3 Precedencia de Operadores
Una expresión está compuesta por operadores, variables y constantes. Para
simplificar, podemos pensar que la forma en la que C evalúa esta expresión es
dividiendo el todo en subexpresiones. Las reglas que definen que subexpresión
evaluar primero, se denominan reglas de precedencia. Aunque siempre podemos
alterar dichas reglas mediante la utilización de paréntesis. En la siguiente tabla
detallamos la precedencia entre los operadores de C.
Tabla 3.6: Precedencia de operadores Mayor precedencia
()[]-.
! ++ * & sizeof (operadores unarios)
*/%
+== !=
&
&&
?:
= += -= *= /= %=
,
Menor precedencia
Precedencia de operadores
La interpretación de cualquier expresión en C++ está determinada por la
precedencia y asociatividad de los operadores en dicha expresión. Cada operador
tiene una precedencia, y los operadores en una expresión se evalúan en orden de
mayor a menor precedencia. La evaluación de operadores con la misma
precedencia viene determinada por su asociatividad. Y, al igual que en
matemáticas, los paréntesis anulan las reglas de precedencia.
En la siguiente tabla se listan los operadores en C++, su precedencia y su
asociatividad. Los operadores se listan en orden de prioridad decreciente (los
situados más arriba tienen mayor prioridad). Los operadores en la misma línea
horizontal tienen la misma precedencia.
Operador Propósito Asociatividad
Scope (unario) De derecha a izquierda
Scope (binario) De izquierda a derecha
. Selección de miembros De izquierda a derecha
[] Índices De izquierda a derecha
() Llamada a función De izquierda a derecha
++ Postincremento De izquierda a derecha
— Postdecremento De izquierda a derecha
sizeof Tamaño de un objeto De derecha a izquierda
++ Preincremento De derecha a izquierda
— Predecremento De derecha a izquierda
& + - ! ~ Operadores unarios De derecha a izquierda
new Crea un objeto De derecha a izquierda
delete Borra un objeto De derecha a izquierda
() Conversión de tipo (type cast) De derecha a izquierda
* .* Puntero a un miembro De izquierda a derecha
/ % Operadores multiplicativos De izquierda a derecha
+ - Operadores aditivos De izquierda a derecha
« » Operadores bitwise De izquierda a derecha
< > <= >= Operadores de relación De izquierda a derecha
== != Operadores de igualdad De izquierda a derecha
& Y bitwise De izquierda a derecha
^ bitwise O exclusivo De izquierda a derecha
| bitwise O inclusivo De izquierda a derecha
&& Y lógico De izquierda a derecha
?: Operador condicional De derecha a izquierda
= *= /= += -= >*gt;=
&= ^= |= %= <<= Operadores de asignación De derecha a izquierda
, Operador coma De derecha a izquierda
4.4 Analizador Sintactico
Un analizador sintáctico ( Parser ) es un programa que reconoce si una o
varias cadenas de caracteres forman parte de un determinado lenguaje. Los
lenguajes habitualmente reconocidos por los analizadores sintácticos son los
lenguajes libres de contexto. Cabe notar que existe una justificación formal que
establece que los lenguajes libres de contexto son aquellos reconocibles por un
autómata de pila, de modo que todo analizador sintáctico que reconozca un
lenguaje libre de contexto es equivalente en capacidad computacional a un
autómata de pila. Los analizadores sintácticos fueron extensivamente estudiados
durante los años 70 del siglo XX, detectándose numerosos patrones de
funcionamiento en ellos, cosa que permitió la creación de programas generadores
de analizadores sintáticos a partir de una especificación de la sintaxis del lenguaje,
tales y como yacc, GNU bison y javacc.
4.4.1 Analizador Descendente Ll
Análisis Sintáctico Predictivo Recursivo
La siguiente fase en la construcción del analizador es la fase de análisis sintáctico.
Esta toma como entrada el flujo de terminales y construye como salida el árbol de
análisis sintáctico abstracto.
El árbol de análisis sintáctico abstracto es una representación compactada del
árbol de análisis sintáctico concreto que contiene la misma información que éste.
Existen diferentes métodos de análisis sintáctico. La mayoría caen en una de dos
categorías: ascendentes y descendentes. Los ascendentes construyen el árbol
desde las hojas hacia la raíz. Los descendentes lo hacen en modo inverso. El que
usaremos aqui es uno de los más sencillos: se denomina método de análisis
predictivo descendente recursivo.
4.4.2 Analizador Ascendente Lr Lalr
Analizador Ascendente LR
Intenta construir un árbol de análisis sintáctico, empezando desde la raíz y
descendiendo hacia las hojas. Lo que es lo mismo que intentar obtener una
derivación por la izquierda para una cadena de entrada, comenzando desde la raíz
y creando los nodos del árbol en orden previo.
4.5 Administración Tabla De Símbolos
“La tabla de símbolos registra información acerca de cada nombre de símbolo en
un programa. Históricamente, los nombres se llamaron símbolos, más que de una
tabla de nombres. En este capítulo, la palabra símbolo significa nombre. La fase
de análisis semántico crea la tabla de símbolos, puesto que no es sino hasta este
análisis que se tiene la suficiente información sobre un nombre para describirlo. La
generación de código usa la tabla de símbolos para extraer las directivas del
ensamblador, para el tipo y para el tamaño apropiados.”
Una tabla de símbolos es una estructura de datos que contiene un registro por
cada identificador. El registro incluye los campos para los atributos del
identificador.
El administrador de la tabla de símbolos se encarga de manejar los accesos a la
tabla de símbolos, en cada una de las etapas de compilación de un programa.
4.6 Manejo Errores Sintácticos y Recuperación
Hay dos conceptos fundamentales:
Corrección de errores: exige que el programa pueda ejecutarse. Suele utilizarse
en sistemas que generán .EXE directamente, pues ahorra tiempo (permite
encontrar errores de ejecución a la vez que los de compilación).
Recuperación de errores: sólo trata de evitar que el número de mensajes de error
sea demasiado grande y que el compilador/intérprete pueda seguir ejecutándose
correctamente en instrucciones sucesivas.
Corrección ortográfica
Errores ortográficos típicos:
Un carácter por otro.
Un carácter perdido.
Un carácter añadido.
Dos caracteres intercambiados.
Pueden comprobarse sólo los errores anteriores, lo que acelera el proceso.
Correcciones posibles:
Análisis sintáctico
Si se espera una palabra reservada y aparece un identificador, buscar la palabra
reservada más parecida al identificador.
Deshacer errores de concatenación. Por ejemplo, convertir ‘begina’ en ‘begin a’.
Análisis semántico
Si un identificador se utiliza en un contexto incompatible con su tipo, tratar de
sustituirlo por otro de nombre parecido y tipo compatible con el contexto.
Si un identificador no ha sido referenciado o asignado, es candidato para
corrección ortográfica. Sólo en compiladores de dos pasos. En la tabla de
símbolos se puede añadir como valor un par de contadores de uso y asignación.
Todas las correcciones efectuadas deben ser cuidadosamente documentadas,
para evitar que el programador se pierda al probar el programa. Corrección de
errores sintácticos
Si se detecta al analizar la cadena
xUy
donde x,y en A* y U en A es el próximo símbolo a analizar, podemos intentar lo
siguiente:
Borrar U e intentarlo de nuevo.
Insertar una cadena de terminales z entre x, U y empezar a analizar a partir de z.
Insertar una cadena de terminales z entre x, U y empezar a analizar a partir de U,
poniendo z en la pila (si es análisis bottom-up).
Borrar símbolos del final de x e intentar de nuevo.
No hacer nunca los dos últimos. Deshace la información semántica asociada.
Ejemplo: tenemos
if (…) { x=0; else …
El error se detecta en “else”. Solución posible: añadir “}” delante de “else”,
analizando
if (…) { x=0; } else …
Recuperación de errores de compilación
Conviene tener una sola rutina de recuperación de errores separada del resto del
compilador.
Evitar que un solo error produzca varios mensajes.
Ejemplo: A[i1,i2,…,i3], donde A no es un “array”. Al abrir el corchete nos dará un
error: “A no es un array”. Al cerrar el corchete podría dar otro: “El número de
índices no coincide con el rango de A”. Si se ha dado el primero, el segundo es
innecesario. Una solución: detectado el primer error, se sustituye la referencia a A
por una referencia a un identificador “fantasma”. La rutina de recuperación de
errores podría ignorar los mensajes que se refieren al identificador fantasma.
Evitar que un error idéntico repetido produzca varios mensajes. Ejemplo:
{…
{ int i;
…
/* { */ …
}
for (i=0; i<j; i++) { a=j-i+1; b=2*a+i; }
}
}
Se nos olvida poner la tercera llave. La llave siguiente cierra el segundo bloque. “i”
está indefinida en el primer bloque. El bucle “for” nos daría cinco veces el mensaje
“variable i indefinida”.
Solución: crear un identificador llamado “i” en la tabla de símbolos con los atributos
correctos. Esto elimina los mensajes subsiguientes. Atención: esto podría hacer
que no se detecte un error real que nos interesaría atrapar. Otra alternativa sería
imprimir un solo mensaje diciendo que el identificador “i” ha sido utilizado sin
declaración previa en las líneas número a,b,c…
Recuperación de errores en un intérprete
Hay que señalar el error y detener la ejecución, permitiendo al programador
revisar las variables
revisar el código
modificar el código
reanudar la ejecución
saltarse líneas
abandonar la ejecución del último programa
abandonar totalmente la ejecución
y asegurarse de que todo sigue correctamente. En lenguaje simbólico se puede
manipular la pila de ejecución, salir automáticamente de rutinas pendientes, sin
continuar la ejecución, etc .
4.7 Generadores Codigo para Analizadores Sintacticos Yacc Bison
La entrada a programas de computación generalmente tiene una estructura
determinada; de hecho, cada programa de computación que recibe una entrada
puede ser visto como definidor de un “lenguaje de entrada” que acepta. Un
lenguaje de entrada puede ser tan complejo como un lenguaje de programación, o
tan simple como una secuencia de números. Desafortunadamente, las facilidades
usuales para la entrada son limitadas, difíciles de usar y generalmente negligentes
en cuanto al chequeo y validación de la entrada.
YACC provee una herramienta general para describir la entrada de un programa
de computación. El usuario de YACC especifica las estructuras de su entrada,
junto con el código que será invocado en la medida en que cada una de esas
estructuras es reconocida. YACC convierte esa especificación en una subrutina
que maneja el proceso de entrada, frecuentemente es conveniente y apropiado
tener el mayor control en la aplicación del usuario manejada por esta subrutina.
La subrutina de entrada producida por YACC llama a la rutina provista por el
usuario para devolver el próximo ítem básico de la entrada. Así, el usuario puede
especificar su entrada en términos de caracteres individuales de entrada, o en
términos de constructor de mayor nivel, tales como nombres y números. La rutina
provista por el usuario podría también manipular rasgos idiomáticos tales como
comentarios y convenciones de continuación, que comúnmente desafían las
especificaciones gramaticales sencillas.
GNU Bison es un generador de parser de propósito general que convierte una
descripción gramatical desde una gramática libre de contexto LALR en un
programa en C para hacer el parse. Es utilizado para crear parsers para muchos
lenguajes, desde simples calculadoras hasta lenguajes complejos.
GNU Bison tiene compatibilidad con Yacc: todas las gramáticas bien escritas para
Yacc, funcionan en Bison sin necesidad de ser modificadas. Cualquier persona
que esté familiarizada con Yacc podría utilizar Bison sin problemas. Es necesaria
experiencia con C para utilizar Bison.
Unidad 5 Análisis semántico
5.1 Analizador Semántico
Se compone de un conjunto de rutinas independientes, llamadas por los
analizadores morfológico y sintáctico.
El análisis semántico utiliza como entrada el árbol sintáctico detectado por el
análisis sintáctico para comprobar restricciones de tipo y otras limitaciones
semánticas y preparar la generación de código.
En compiladores de un solo paso, las llamadas a las rutinas semánticas se
realizan directamente desde el analizador sintáctico y son dichas rutinas las que
llaman al generador de código. El instrumento más utilizado para conseguirlo es la
gramática de atributos.
En compiladores de dos o más pasos, el análisis semántico se realiza
independientemente de la generación de código, pasándose información a través
de un archivo intermedio, que normalmente contiene información sobre el árbol
sintáctico en forma linealizada (para facilitar su manejo y hacer posible su
almacenamiento en memoria auxiliar).
En cualquier caso, las rutinas semánticas suelen hacer uso de una pila (la pila
semántica) que contiene la información semántica asociada a los operandos (y a
veces a los operadores) en forma de registros semánticos.
Propagación de atributos Sea la expresión
int a,b,c;
a/(b+c^2)
El árbol sintáctico es:
/
--------|
|
a
+
--------|
|
b
^
--------|
|
c
2
De la instrucción declarativa, la tabla de símbolos y el analizador morfológico
obtenemos los atributos de los operandos:
/
--------|
|
a
+
int --------|
|
b
^
int --------|
|
c
2
int int
Propagando los atributos obtenemos:
/ int
--------|
|
a
+ int
int ---------
|
|
b
^ int
int --------|
|
c
2
int int
Si la expresión hubiera sido
a/(b+c^−2)
El árbol sintáctico sería el mismo, sustituyendo 2 por −2. Sin embargo, la
propagación de atributos sería diferente:
/ real
--------|
|
a
+ real
int --------|
|
b
^ real
int --------|
|
c
−2
int int
En algún caso podría llegar a producirse error (p.e. si / representara sólo la
división entera).
Si la expresión hubiera sido
int a,b,c,d;
a/(b+c^d)
El árbol sintáctico sería el mismo, sustituyendo 2 por d. Sin embargo, la
propagación de atributos sería incompleta:
/ {int,real}
--------|
|
a
+ {int,real}
int --------|
|
b
^ {int,real}
int --------|
|
c
d
int int
El analizador semántico podría reducir los tipos inseguros al tipo máximo (real) o
utilizar un tipo interno nuevo (ej. arit={int,real}, una unión).
Lo anterior es un ejemplo de propagación bottom-up. La propagación top-down
también es posible: lo que se transmite son las restricciones y los tipos de las
hojas sirven de comprobación. Por ejemplo, si la división sólo puede ser entera,
transmitimos hacia abajo la restricción de que sus operandos sólo pueden ser
enteros. Al llegar a d, esa restricción se convierte en que d debe ser positiva. Si no
lo es, error.
La implantación de todos los casos posibles de operación con tipos mixtos podría
ser excesivamente cara. En su lugar, se parte de operaciones relativamente
simples (ej. int+int, real+real) y no se implementan las restantes (ej. int+real,
real+int), añadiendo en su lugar operaciones monádicas de cambio de tipo (ej.
int→real).
Esta decisión puede introducir ambigüedades. Por ejemplo, sea el programa
real a;
int b,c;
a:=b+c
El árbol sintáctico es:
:=
--------|
|
a
+
real --------|
|
b
c
int int
Existen dos conversiones posibles:
:= real
:= real
----------------|
|
|
|
a
+ real
a
+ int
real --------real --------|
|
|
|
b
c
b
c
int int
int int
El problema es que no tenemos garantía de que los dos procedimientos sean
equivalentes. El segundo puede dar overflow, el primero pérdida de precisión. La
definición del lenguaje debe especificar estos casos.
Las transformaciones posibles se pueden representar mediante un grafo cuyos
nodos son los tipos de datos y cada arco indica una transformación. Dado un
operando de tipo A que se desea convertir al tipo B, se trata de encontrar una
cadena de arcos que pase de A a B en el grafo anterior. Podría haber varios
grafos, cada uno de los cuales se aplicará en diferentes condiciones, por ejemplo,
uno para las asignaciones, otro para las expresiones, etc.
5.2 Verificacion Tipos en Expresiones
5.3 Conversión de Tipos
Hay situaciones en las cuales se tiene un valor de un tipo dado y se desea
almacenar ese valor en una variable de un tipo diferente. En algunos tipos es
posible almacenar simplemente el valor sin una conversión de tipos; lo que se
denomina conversión automática. Esto sólo es posible en Java si el compilador
reconoce que la variable destino tiene la suficiente precisión para contener el valor
origen, como almacenar un valor byte en una variable int. A esto se le llama
ensanchamiento o promoción, dado que el tipo más pequeño se ensancha o
promociona al tipo compatible más grande. Si por el contrario, se desea asignar un
valor de variable int a una variable byte se necesita realizar una conversión de
tipos explícita. A esto se le llama estrechamiento, dado que se estrecha
explícitamente el valor para que quepa en el destino. La conversión de un tipo se
realiza poniendo delante un nombre de tipo entre paréntesis, por ejemplo, (tipo)
valor. El código siguiente demuestra la conversión de tipos de int a byte. Si el valor
del entero fuese mayor que el rango de un byte, se reduciría al módulo (resto de la
división) del rango de byte.
int a = 100;
byte b = (byte) a;
5.4 Acciones Agregadas Analizador Sintactico Descendente (top-down)
• Muchas de las actividades que realiza un analizador semántico no son
estándares, dependerán del objetivo del lenguaje de programación; por ejemplo,
en algunas aplicaciones es interesante conocer que los datos estén en algún
rango válido o que ciertos valores se utilicen para uso reservado
Acciones agregadas a un analizador semántico
• En algunas ocasiones nos interesa conocer el significado de las palabras de
algún lenguaje dependiendo del contexto (gramáticas de tipo 1) para diferenciar
palabras polisemánticas.
5.5 Pila Semantica En Analizador Sintactico Ascendente (bottom-up)
• El diseño ascendente se refiere a la identificación de aquellos procesos que
necesitan computarizarse con forme vayan apareciendo, su análisis como sistema
y su codificación, o bien, la adquisición de paquetes de software para satisfacer el
problema inmediato.
Pila semántica
• Los problemas de integración entre los subsistemas son sumamente costosos y
muchos de ellos no se solucionan hasta que la programación alcanza la fecha
limite para la integración total del sistema.
• Se necesita una memoria auxiliar que nos permita guardar los datos intermedios
para poder hacer la comparación.
5.6 Administración Tabla de Símbolos
“La tabla de símbolos registra información acerca de cada nombre de símbolo en
un programa. Históricamente, los nombres se llamaron símbolos, más que de una
tabla de nombres. En este capítulo, la palabra símbolo significa nombre. La fase
de análisis semántico crea la tabla de símbolos, puesto que no es sino hasta este
análisis que se tiene la suficiente información sobre un nombre para describirlo. La
generación de código usa la tabla de símbolos para extraer las directivas del
ensamblador, para el tipo y para el tamaño apropiados.”
Una tabla de símbolos es una estructura de datos que contiene un registro por
cada identificador. El registro incluye los campos para los atributos del
identificador.
El administrador de la tabla de símbolos se encarga de manejar los accesos a la
tabla de símbolos, en cada una de las etapas de compilación de un programa.
5.7 Manejo Errores Semánticos
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:
El compilador debe ser capaz de detectar errores en la entrada;
El compilador debe recuperarse de los errores sin perder demasiada información;
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.
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:
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:
Errores de ortografía y
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:
Errores Léxicos;
Errores Sintácticos;
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.
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.
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:
Al menos el 40% de los programas compilados eran sintáctica o semánticamente
incorrectos.
Un 80% de las proposiciones incorrectas sólo tenían un error.
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.
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%.
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:
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:
El proceso de compilación de detiene al ocurrir el error y el compilador debe
informar del error.
El proceso de compilación continúa cuando ocurre el error y se informa de éste en
un archivo de listado.
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.
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:
Nombres ilegales de identificadores: un nombre contiene caracteres inválidos;
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;
Cadenas incorrectas de caracteres: una cadena de caracteres es demasiado larga
(probablemente por la omisión de comillas que cierran);
Errores de ortografía en palabras reservadas: caracteres omitidos, adicionales,
incorrectos o mezclados;
Etiquetas ilegales: una etiqueta es demasiado larga o contiene caracteres
inválidos;
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.
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:
Paréntesis o corchetes omitidos, por ejemplo, x : = y * (1 + z;
Operadores u operando omitidos, por ejemplo, x : = y (1 + z );
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:
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.
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.
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 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:
Identificadores no definidos;
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.
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;
Figura 6.3 Uso del procedimiento de prueba
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.
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.
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
ELSE Error(...) END;
UNTIL (símbolo <> ident);
END;
Figura 6.5 Código modificado para el análisis des de constantes
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 de la figura 6.7, 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;
Unidad 6 Generación de código
6.1 Lenguajes Intermedios
Lenguaje Intermedio. Tipo (1)
Es una representación más abstracta y uniforme que un lenguaje máquina
concreto. Su misión es descomponer las expresiones complejas en binarias y las
sentencias complejas en sentencias simples. Ventajas: • Permite una fase de
análisis (análisis semántico) independiente de la máquina. • Se pueden realizar
optimizaciones sobre el código intermedio (Las complejas rutinas de optimización
son independientes de la máquina). Desventajas: • Pérdida de eficiencia (no
permite una compilación de una sola pasada). • Introduce en el compilador una
nueva fase de traducción.
Lenguaje Intermedio. Tipo (2)
Tipos de lenguajes intermedios: • Árbol sintáctico. • Árbol sintáctico abstracto. �
Todos los nodos del árbol representan símbolos terminales. � Los nodos hijos son
operandos y los nodos internos son operadores. • Grafo dirigido acíclico (GDA). •
Notación posfija. • Definición de una máquina abstracta. • N-tupla: � Cada
sentencia del lenguaje intermedio consta de N elementos: (Operador, Operando1,
Operando2, … , Operando N?−1) � Los más usuales son los tercetos (tripletas) y
los cuartetos (cuádruplas), llamados también código de tres direcciones.
Lenguaje Intermedio. Tipo (3)
Tripletas: Ejemplo: d = a + b * c [1] (*, b, c) [2] (+, a, [1]) [3] (=, d, [2]) <operador>,
<operando_1>, <operando_2> Cuartetos: Ejemplo: d = a + b * c (*, b, c, temp1) (+,
a, temp1, temp2) (=, temp2, —, d)
6.2 Notaciones Lenguajes Intermedios
6.2.1 Infija Lenguajes Intermedios
6.2.2 Postfija Lenguajes Intermedios
6.2.3 Prefija Lenguajes Intermedios
6.3 Representación Código Intermedio
6.3.1 Notación Polaca
La notación polaca es la originada por un Autómata con pila, en la que los
operadores siempre preceden a los operandos sobre los que actúan, y que tiene la
ventaja de no necesitar paréntesis:
Estandar
Ejemplo 1: 2 * ( 3 + 5 )
Ejemplo 2: 2 * 3 + 5
Polaca
Ejemplo 1: * 2 + 3 5
Ejemplo 2: + * 2 3 5
6.3.2 Código P
Código P o Prolog
Prolog, proveniente del francés Programation et Logique, es un lenguaje de
programación lógico e interpretado, bastante popular en el medio de investigación
en Inteligencia Artificial.
Se trata de un lenguaje de programación ideado a principios de los años 70 en la
universidad de Aix-Marseille por los profesores Alain Colmerauer y Phillipe
Roussel. Inicialmente se trataba de un lenguaje totalmente interpretado hasta que,
a mediados de los 70, David H.D. Warren desarrolló un compilador capaz de
traducir Prolog en un conjunto de instrucciones de una máquina abstracta
denominada Warren Abstract Machine, o abreviadamente, WAM. Desde entonces
Prolog es un lenguaje semi-interpretado.
Prolog se enmarca en el paradigma de los lenguajes lógicos, lo que lo diferencia
enormemente de otros lenguajes más populares tales como Fortran, Pascal, C,
etc.
En todos los mencionados, las instrucciones se ejecutan normalmente en orden
secuencial, es decir, una a continuación de otra, en el mismo orden en que están
escritas, que sólo varía cuando se alcanza una instrucción de control (un bucle,
una instrucción condicional o una transferencia).
Los programas en Prolog se componen de cláusulas de Horn que constituyen
reglas del tipo “modus ponendo ponens”, es decir, “Si es verdad el antecendente,
entonces es verdad el consecuente”. No obstante, la forma de escribir las
cláusulas de Horn es al contrario de lo habitual. Primero se escribe el consecuente
y luego el antecedente. El antecedente puede ser una conjunción de condiciones
que se denomina secuencia de objetivos. Cada objetivo se separa con una coma y
puede considerarse similar a una instrucción o llamada a procedimiento de los
lenguajes imperativos. En Prolog no existen instrucciones de control. Su ejecución
se basa en dos conceptos: la unificación y el backtracking. Gracias a la
unificación, cada objetivo determina un subconjunto de cláusulas susceptibles de
ser ejecutadas. Cada una de ellas se denomina punto de elección. Prolog
selecciona el primer punto de elección y sigue ejecutando el programa hasta
determinar si el objetivo es verdadero o falso. En caso de ser falso entra en juego
el ‘backtracking’, que consiste en deshacer todo lo ejecutado situando el programa
en el mismo estado en el que estaba justo antes de llegar al punto de elección.
Entonces se toma el siguiente punto de elección que estaba pendiente y se repite
de nuevo el proceso. Todos los objetivos terminan su ejecución bien en
“verdadero”, bien en “falso”.
Las listas son colecciones de elementos en Prolog. Una lista se divide en dos
partes: Cabeza. Es el primer elemento de la lista. Cola. Es una lista con el resto de
los elementos de la lista. La cabeza y la cola de una lista se separan con el
símbolo “|”.
Ejemplo de Código Prolog
declaraciones
padrede(‘juan’, ‘maria’). % juan es padre de maria
padrede(‘pablo’, ‘juan’). % pablo es padre de juan
padrede(‘pablo’, ‘marcela’).
padrede(‘carlos’, ‘debora’).
% A es hijo de B si B es padre de A
hijode(A,B) :- padrede(B,A).
% A es abuelo de B si A es padre de C y C es padre B
abuelode(A,B) :padrede(A,C),
padrede(C,B).
% A y B son hermanos si el padre de A es también el padre de B y si A y B no son
lo mismo
hermanode(A,B) :padrede(C,A) ,
padrede(C,B),
A \== B.
% A y B son familiares si A es padre de B o A es hijo de B o A es hermano de B
familiarde(A,B) :padrede(A,B).
familiarde(A,B) :hijode(A,B).
familiarde(A,B) :hermanode(A,B).
consultas
% juan es hermano de marcela?
?- hermanode(‘juan’, ‘marcela’).
yes
% carlos es hermano de juan?
?- hermanode(‘carlos’, ‘juan’).
no
% pablo es abuelo de maria?
?- abuelode(‘pablo’, ‘maria’).
yes
% maria es abuelo de pablo?
?- abuelode(‘maria’, ‘pablo’).
no
Ejemplo sobre Listas Prolog
% Si queremos hallar la longitud de una lista.
% La longitud de una lista vacia es 0.
% La longitud de cualquier lista es la longitud de la cola + 1.
longitud([],0).
longitud([H|T],N):-longitud(T,N0), N is N0 + 1.
?- longitud([a,b,c],L).
3
?- longitud([a,b,c],4).
No
% Si queremos determinar si un elemento es pertenece a una lista.
% El elemento pertenece a la lista si coincide con la cabeza de la lista.
% El elemento pertenece a la lista si es se encuentra en la cola de la lista.
pertenece(X,[X|_]).
pertenece(X,[_|R]):- pertenece(X,R).
?- pertenece(b,[a,b,c]).
Yes
?- pertenece(b,[a,[b,c]]).
No
?- pertenece([b,c],[a,[b,c]]).
Yes
% Si queremos eliminar un elemento de la lista.
% Si X es la cabeza de la lista, la cola T es la lista sin X
% Si X no es la cabeza de la lista, conservamos la cabeza de la lista
% como parte de la respuesta y continuamos eliminando X de la cola T.
elimina (X,[X|T],T).
elimina (X,[H|T],[H|T1]):- elimina (X,T,T1).
?- elimina(1,[1,2,3,4],R).
R = [2,3,4]
?- elimina(1,R,[2,3]).
R = [1, 2, 3]
R = [2, 1, 3]
R = [2, 3, 1]
% Si queremos calcular la inversa de una lista.
% La inversa de una lista vacia es una lista vacia.
% La inversa de H|T es la inversa de T concatenada con H.
inversa([],[]).
inversa([H|T],L):- inversa(T,R), concatenar(R,[H],L).
?- inversa([a,b,c,d],[d,c,b,a]).
6.3.3 Triplos Lenguajes Intermedios
6.3.4 Cuádruplos Lenguajes Intermedios
6.4 Esquemas de Generación Lenguajes Intermedios
Cuando una empresa desarrolla un compilador para un lenguaje fuente y un
lenguaje objeto determinados, normalmente no es el único compilador que la
empresa piensa desarrollar; es más muchos fabricantes de microprocesadores
tienen una división de dedicada a desarrollar compiladores para los nuevos chips
que construya.
Cuando el número de lenguaje fuente crece hasta un número grande M, y/o
cuando el número de lenguajes objeto también crece hasta un número grande N,
es necesario encontrar una técnica para evitar tener que diseñar M x N
compiladores. La solución consiste en utilizar un lenguaje intermedio o una
representación intermedia; de esta forma sólo hay que construir M programas que
traduzcan de cada lenguaje fuente al lenguaje intermedio (los front ende), y N
programas que traduzcan del lenguaje intermedio a cada lenguaje objeto (los back
end).
La matemática (del lat. mathematĭca, y este del gr. τὰ μαθηματικά, derivado de
μάθημα, conocimiento) es una ciencia formal que estudia las propiedades y las
relaciones que se pueden establecer entre los entes abstractos, como los
símbolos, los números y las figuras geométricas.[1]
Aunque la matemática sea la supuesta “Reina de las Ciencias”, algunos
matemáticos no la consideran una ciencia natural. Principalmente, los
matemáticos definen e investigan estructuras y conceptos abstractos por razones
puramente internas a la matemática, debido a que tales estructuras pueden
proveer, por ejemplo, una generalización elegante, o una herramienta útil para
cálculos frecuentes. Además, muchos matemáticos consideran la matemática
como una forma de arte en vez de una ciencia práctica o aplicada. Sin embargo,
las estructuras que los matemáticos investigan frecuentemente sí tienen su origen
en las ciencias naturales, y muchas veces encuentran sus aplicaciones en ellas,
particularmente en la física.
No existe un único lenguaje intermedio en todos los compiladores, sino que cada
empresa que diseña compiladores suele tener su propio lenguaje intermedio. La
utilización de un lenguaje intermedio permite construir en, mucho menos tiempo un
compilador para otra máquina y también permite construir compiladores para otros
lenguajes fuente generando códigos para la misma máquina. La matemática es un
arte, pero también una ciencia de estudio. Informalmente, se puede decir que es el
estudio de los “números y símbolos”. Es decir, es la investigación de estructuras
abstractas definidas a partir de axiomas, utilizando la lógica y la notación
matemática. Es también la ciencia de las relaciones espaciales y cuantitativas. Se
trata de relaciones exactas que existen entre cantidades y magnitudes, y de los
métodos por los cuales, de acuerdo con estas relaciones, las cantidades buscadas
son deducibles a partir de otras cantidades conocidas o presupuestas.
Véase también: Filosofía de la matemática No es infrecuente encontrar a quien
describe la matemática como una simple extensión de los lenguajes naturales
humanos, que utiliza una gramática y un vocabulario definidos con extrema
precisión, cuyo propósito es la descripción y exploración de relaciones
conceptuales y físicas. Recientemente, sin embargo, los avances en el estudio del
lenguaje humano apuntan en una dirección diferente: los lenguajes naturales
(como el español y el francés) y los lenguajes formales (como la matemática y los
lenguajes de programación) son estructuras de naturaleza básicamente diferente.
Por ejemplo, el compilador de C de GNU que se distribuye con Linux es una
versión de una familia de compiladores de C para diferentes máquinas o sistemas
operativos: Alpha, AIX, Sun, HP, MS-DOS, etc.. Además, GNU ha desarrollado un
compilador de FORTRAN y otro de Pascal que, al utilizar el mismo lenguaje
intermedio, pueden ser portados a todos los sistemas y máquinas en las que y a
existe un compilador de C de GNU con relativamente poco esfuerzo.
La generación de código intermedio transforma un árbol de análisis sintáctico
(semántico) en una representación en un lenguaje intermedio, que suele ser
código suficientemente sencillo para poder luego generar código máquina.
Una forma de hacer esto es mediante el llamado código de tres direcciones. Una
sentencia en código de tres direcciones es: A := B op C, donde A, B y C son
operandos y op es un operador binario. También permite condiciones simples y
saltos. Por ejemplo, para la siguiente sentencia:
WHILE (A > B) AND (A < = 2 * B – 5) DO A : = A + B
el código intermedio generado ( código en tres direcciones) será:
L1 : IF A > B GOTO L2
GOTO L3
L2 : T1 : = 2 * B
(*nivel más alto que ensamblador*)
T2 : = T1 – 5
(*pero más sencillo que Pascal*)
IF A < T2 GOTO L4
GOTO L3
L4 : A : = A + B
GOTO L1
L3 : . . . . . . .
APLICACIONES
La importancia practica de lenguaje en la informática se manifiesta principalmente
en el uso cotidiano que hace el profesional informático de compiladores e
interpretes, consustancial al la gestión y programación de los sistemas
informáticos. Así pues, un conocimiento acerca del funcionamiento interno de
estas herramientas básicas resulta fundamental. Pero los conocimientos
adquiridos en su estudio encuentren aplicación fuera del campo de la compilación.
Es probable que ocas personas realice o mantenga un compilador para un
lenguaje de programación, pero mucha gente puede obtener provecho del uso de
un gran número de sus técnicas para el diseño de software en general.
En efecto, entre los campos de la informática en los que encuentra aplicación las
técnicas aprendidas en COMPILADORES e INTÉRPRETES se puede citar lo
siguiente:
Tratamiento de ficheros de texto con información estructurada. Lenguaje como
Perl y TEL, o comandos como el sed o egrep de UNIX, incorpora tratamiento de
expresiones regulares para la detección y/o modificación de patrones sin texto.
Procesadores de texto. Procesadores como vi o Emacs incorporan también la
posibilidad de efectuar búsquedas y sustituciones mediante expresiones regulares.
Existen también procesadores (entre ellos los Emacs) capaces de analizar y tratar
ficheros de texto de organización compleja.
Diseño e interpretación de lenguaje para formateo y texto y descripción de
gráficos. Sistema de formateo de texto (como el HTML o el TEX) o para la
especificación de tablas (tbl), ecuaciones (eqn), gráficos (postscript), etc. requieren
sofisticados microprocesadores.
Gestión de base de datos. Las técnicas que estamos considerando pueden
explotarse tanto en la exploración y proceso de ficheros de información como en la
realización de la interfase de usuario.
Traducción de formato de fichero.
Calculo simbólico.
Reconocimiento de formas. Las técnicas de análisis sintáctico son ampliamente
utilizadas en la detección de patrones en texto, el reconocimiento automático del
habla o la visión por computador.
6.4.1 Expresiones Lenguajes Intermedios
6.4.2 Declaración Variables Constantes Lenguajes Intermedios
6.4.3 Estatuto Asignación Lenguajes Intermedios
6.4.4 Estatuto Condicional Lenguajes Intermedios
6.4.5 Estatuto Ciclos Lenguajes Intermedios
6.4.6 Arreglos Lenguajes Intermedios
(Intermediate language). En computación, un lenguaje intermedio es el lenguaje
de una máquina abstracta diseñada para ayudar en el análisis de los programas
de computadora. El término viene de su uso en los compiladores, donde un
compilador primero traduce el código fuente de un programa, en una forma más
apropiada para las transformaciones de mejora del código (forma usualmente
llamada bytecode), como un paso intermedio antes de generar el archivo objeto o
el código máquina para una máquina específica.
Una variante del significado de “lenguaje intermedio” se da en esos lenguajes de
alto nivel que no soportan o no generan un archivo objeto o código máquina, pero
sí generan un lenguaje intermedio. Luego ese lenguaje intermedio se transfiere a
un compilador que termina de realizar el archivo objeto o el código máquina. Esto
se emplea generalmente por cuestiones de optimización y portabilidad.
6.4.7 Funciones Lenguajes Intermedios
Función del Lenguaje, entendemos que es el uso de la lengua que hace un
hablante. En simples palabras, las funciones del lenguaje son los diferentes
objetivos, propósitos y servicio que se le da al lenguaje al comunicarse, dándose
una función del lenguaje por cada factor que tiene éste, en donde la función que
prevalece es el factor en donde más se pone énfasis al comunicarse. Diversos
lingüistas (Karl Bühler, Roman Jakobson, Michael Halliday…) han propuesto
distintas clasificaciones de las funciones del lenguaje: Bühler propuso que existían
únicamente tres funciones: • La Representativa (por la cual se trasmiten
informaciones objetivamente) • La Expresiva o emotiva (que expresa sentimientos
del emisor) • La Conativa, mediante la que se influye en el receptor del mensaje a
través de órdenes, mandatos o sugerencias… Este modelo parecía muy
incompleto a Jakobson, quien caracterizó mejor las funciones de Bühler y añadió
otras tres sobre los ejes de los factores de la comunicación :
Función Apelativa o Conativa Se centra en el receptor. Es la función de mandato y
pregunta. Sus recursos lingüísticos son los vocativos, oraciones interrogativas,
utilización deliberada de elementos afectivos, adjetivos valorativos, términos
connotativos y toda la serie de recursos retóricos. Se da en lenguaje coloquial, es
dominante en la publicidad y propaganda política e ideológica en general.
Mediante el uso de esta función se pretende causar una reacción en el receptor.
Es decir con esta función se pretende que haga algo o que deje de hacer. Por
ejemplo cuando decimos “cállate” o “abre la puerta por favor”, etc. El mensaje
solicita la atención del destinatario, es decir apela a él, implicita o explicitamente.
También se le conoce como función apelativa. Funcion Referencial Es la función
del lenguaje en donde se pone énfasis al factor de contexto. Al ser el contexto
todo lo extra comunicativo, la función referencial trata solamente sucesos reales y
comprobables, ya que no son opiniones ni cosas subjetivas, lo que son es una
serie de elementos verificables entre otros Está presente en todos los actos
comunicativos. Se da cuando el mensaje que se transmite puede ser verificable,
porque reconocemos la relación que se establece entre el mensaje y el objeto
(referente) Es aquella que utiliza el lenguaje denotativo (el significado primario de
las palabras). Prevalecen los sustantivos y verbos; los textos informativos,
científicos y periodísticos. Hay 7 funciones: Expresiva o emotiva, apelativa o
conativa, referencial o representativa, situacional, Esta función se centra en el
contexto e identifica la relacion entre el mensaje y el objeto del que se habla; es
decir, se centra en la tercera persona, la lengua se usa para hablar de algo o
alguien, que no somos ni tú ni yo. Así, la función referencial se hace presente en
casi todos los mensajes y sirve para evaluar si éstos son objetivos o no.
Unidad 7 Optimización
7.1 Tipos Optimización
•La optimización va a depender del lenguaje de programación y es directamente
proporcional al tiempo de compilación; es decir, entre más optimización mayor
tiempo de compilación.
•Las optimizaciones pueden realizarse de diferentes formas. Las optimizaciones
se realizan en base al alcance ofrecido por el compilador de programación y es
directamente proporcional al tiempo de compilación; es decir, entre más
optimización mayor tiempo de compilación.
•Como el tiempo de optimización es gran consumidor de tiempo (dado que tiene
que recorrer todo el árbol de posibles soluciones para el proceso de optimización)
la optimización se deja hasta la fase de prueba final.
•Algunos editores ofrecen una versión de depuración y otra de entrega o final.
•La optimización es un proceso que tiene a minimizar o maximizar alguna variable
de rendimiento, generalmente tiempo, espacio, procesador, etc.
•Desafortunamente no existen optimizador que hagan un programa más rápido y
que ocupe menor espacio.
•La optimización se realiza reestructurando el código de tal forma que el nuevo
código generado tenga mayores beneficios. La mayoría de los compiladores
tienen una optimización baja, se necesita de compiladores especiales para
realmente optimizar el código.
7.1.1 Locales Optimización
La optimización local se realiza sobre módulos del programa. En la mayoría de las
ocasiones a través de funciones, métodos, procedimientos, clases, etc.
La característica de las optimizaciones locales es que sólo se ven reflejados en
dichas secciones.
Optimización Local
La optimización local sirve cuando un bloque de programa o sección es crítico por
ejemplo: la E/S, la concurrencia, la rapidez y confiabilidad de un conjunto de
instrucciones.
Como el espacio de soluciones es más pequeño la optimización local es más
rápida.
Locales Optimización
La optimización local se realiza sobre módulos del programa. En la mayoría de las
ocasiones a través de funciones, métodos, procedimientos, clases, etc.
La característica de las optimizaciones locales es que sólo se ven reflejados en
dichas secciones.
Optimización Local
La optimización local sirve cuando un bloque de programa o sección es crítico por
ejemplo: la E/S, la concurrencia, la rapidez y confiabilidad de un conjunto de
instrucciones.
Como el espacio de soluciones es más pequeño la optimización local es más
rápida.
7.1.2 Bucles Optimización
•Los ciclos son una de las partes más esenciales en el rendimiento de un
programa dado que realizan acciones repetitivas, y si dichas acciones están mal
realizadas, el problema se hace N veces más grandes.
•La mayoría de las optimizaciones sobre ciclos tratan de encontrar elementos que
no deben repetirse en un ciclo.
Ciclos
while(a == b)
{ int c = a; c = 5; …; }
En este caso es mejor pasar el int c =a; fuera del ciclo de ser posible.
Ciclos
•El problema de la optimización en ciclos y en generalradica es que muy difícil
saber el uso exacto de algunas instrucciones. Asíque no todo código de proceso
puede ser optimizado. •Otros uso de la optimización pueden ser el mejoramiento
de consultas en SQL o en aplicaciones remotas (sockets, E/S, etc.)
7.1.3 Globales Optimización
variables y eliminarlas toma su tiempo) pero consume más memoria.
•Algunas optimizaciones incluyen utilizar como variables registros del CPU, utilizar
instrucciones en ensamblador.
7.1.4 De Mirilla Optimización
El pensamiento crítico es un elemento importante para el éxito en la vida (Huitt,
1993; Thomas y Smoot, 1994). Una Definicion Propuesta: El pensamiento critico
debe ser contrastado con el pensamiento no-critico. pensamiento habitual o
rutinario.
la lluvia de ideas
pensamiento creativo
pensamiento prejuicioso el pensamiento emocional el pensamiento intuitivo
La definicion de Huitt:
El pensamiento crítico es la actividad mental disciplinada de evaluar los
argumentos o proposiciones haciendo juicios que puedan guiar el desarrollo de las
creencias y la toma de acción to:
7.1.4 Optimización de Mirilla
•La optimización de mirilla trata deestructurar de manera eficiente el flujo del
programa, sobre todo en instrucciones de bifurcación como son las decisiones,
ciclos y saltos de rutinas.
•La idea es tener los saltos lo más cerca de las llamadas, siendo el salto lo más
pequeño posible.
7.2 Costos Optimización
•Los costos son el factor más importante a tomar en cuentaa la hora de optimizar
ya que en ocasiones la mejora obtenida puede verse no reflejada en el programa
finalpero si ser perjudicial para el equipo de desarrollo.
•La optimización de una pequeña mejora tal vez tenga una pequeña ganancia en
tiempo o en espacio pero sale muy costosa en tiempo en generarla.
•Pero en cambio si esa optimización se hace por ejemplo en un ciclo, la mejora
obtenida puede ser N veces mayor por lo cual el costo se minimiza y es benéfico
la mejora.
•Por ejemplo: for(int i=0; i< 10000; i++); si la ganancia es de 30 ms 300s.
7.2.1 Costo de Ejecución Optimización
Los costos de ejecución son aquellos que vienen implícitos al ejecutar el
programa.
•En algunos programas se tiene un mínimo para ejecutar el programa, por lo que
el espacio y la velocidad del microprocesadores son elementos que se deben
optimizar para tener un mercado potencial más amplio.
•Las aplicaciones multimedios como los videojuegos tienen un costo de ejecución
alto por lo cual la optimización de su desempeño es crítico, la gran mayoría de las
veces requieren de procesadores rápidos (e.g. tarjetas de video) o de mucha
memoria.
•Otro tipo de aplicaciones que deben optimizarse son las aplicaciones para
dispositivos móviles.
•Los dispositivos móviles tiene recursos más limitados que un dispositivo de
cómputo convencional razón por la cual, el mejor uso de memoriay otros recursos
de hardware tiene mayor rendimiento.
•En algunos casos es preferible tener la lógica del negocio más fuerte enotros
dispositivos y hacer uso de arquitecturas descentralizadas como cliente/servidor o
P2P.
7.2.2 Criterios para Mejorar Código
•La mejor manera de optimizar el código es hacer ver a los programadores que
optimicen su código desde el inicio, el problema radica en que el costo podría ser
muy grande ya que tendría que codificar más y/o hacer su código mas legible.
•Los criterios de optimización siempre están definidos por el compilador.
•Muchos de estos criterios pueden modificarse con directivas del compilador
desde el código o de manera externa.
•Este proceso lo realizan algunas herramientas del sistema como los ofuscadores
para códigomóvil y código para dispositivos móviles.
7.2.3 Herramientas para Análisis del Flujo de Datos
•Existen algunas herramientas que permiten el análisis de los flujos de datos, entre
ellas tenemos los depuradores y desambladores.
•La optimización al igual que la programación es un arte y no se ha podido
sistematizar del todo.
Unidad 8 Generación de código objeto
8.1 Lenguaje Maquina Características
Un lenguaje de programación de bajo nivel es el que proporciona poca o ninguna
abstracción del microprocesador de un ordenador. Consecuentemente es
fácilmente trasladado a lenguaje de máquina.
La palabra “bajo” no implica que el lenguaje sea inferior a un lenguaje de alto nivel;
se refiere a la reducida abstracción entre el lenguaje y el hardware.
Uso: ventajas e inconvenientes. En general se utiliza este tipo de lenguaje para
programar controladores (drivers).
La programación en un lenguaje de bajo nivel como el lenguaje de la máquina o el
lenguaje simbólico tiene ciertas ventajas:
Mayor adaptación al equipo.
Posibilidad de obtener la máxima velocidad con mínimo uso de memoria.
Pero también tiene importantes inconvenientes:
Imposibilidad de escribir código independiente de la máquina.
Mayor dificultad en la programación y en la comprensión de los programas.
El programador debe conocer más de un centenar de intrucciones.
Es necesario conocer en detalle la arquitectura de la máquina.
Características
Se trabaja a nivel de instrucciones, es decir, su programación es al más fino
detalle. Está orientado a la máquina.
Primera generación
El lenguaje de programación de primera generación (por sus siglas en inglés,
1GL), es el lenguaje de código máquina. Es el único lenguaje que un
microprocesador entiende de forma nativa. El lenguaje máquina no puede ser
escrito o leído usando un editor de texto, y por lo tanto es raro que una persona lo
use directamente.
Segunda generación
El lenguaje de programación de segunda generación (por sus siglas en inglés,
2GL), es el lenguaje ensamblador. Se considera de segunda generación porque,
aunque no es lenguaje nativo del microprocesador, un programador de lenguaje
ensamblador debe conocer la arquitectura del microprocesador (como por ejemplo
las particularidades de sus registros o su conjunto de instrucciones).
8.1.2 Direccionamiento Lenguaje Maquina
Es la forma en como se accede a la memoria. Recordar que un programa no
puede ejecutarse sino se encuentra en memoria principal. La forma de acceder a
la memoria depende del microprocesador, pero en general existen dos tipos de
direccionamiento: directo e indirecto.
El direccionamiento directo también recibe el nombre de direccionamiento
absoluto y el acceso a las direcciones se hace de manera directa. El
direccionamiento indirecto también recibe el nombre de direccionamiento relativo y
se basa a partir de una dirección genérica, generalmente el inicio del programa.
Para acceder a una dirección relativa se suma a la dirección base el número de
espacios de memorias necesarias. El direccionamiento relativo hace a los
programas relocalizables e independientes. Si la dirección base es el inicio de la
memoria fija el direccionamiento pasa a ser un variante de direccionamiento
absoluto.
8.2 Lenguaje Ensamblador Caracteristicas
El lenguaje Assembly es un tipo de lenguaje de bajo nivel utilizado para escribir
programas informáticos, y constituye la representación más directa del código
máquina específico para cada arquitectura de computadoras legible por un
programador.
Fue usado ampliamente en el pasado para el desarrollo de software, pero
actualmente sólo se utiliza en contadas ocasiones, especialmente cuando se
requiere la manipulación directa del hardware o se pretenden rendimientos
inusuales de los equipos.
Ensambladores [editar]Un ensamblador crea código objeto traduciendo
instrucciones mnemónicas a códigos operativos, e interpretando los nombres
simbólicos para direcciones de memoria y otras entidades. El uso de referencias
simbólicas es una característica básica de los ensambladores, evitando tediosos
cálculos y direccionamiento manual después de cada modificación del programa.
La mayoría de los ensambladores también incluyen facilidades para crear macros ,
a fin de generar series de instrucciones cortas que se ejecutan en tiempo real, en
lugar de utilizar subrutinas[1] .
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 1950. Los ensambladores modernos, especialmente para arquitecturas
basadas en RISC, como por ejemplo MIPS, SPARC y PA-RISC optimizan las
instrucciones para explotar al máximo la eficiencia de segmentación del CPU.
Los ensambladores de alto nivel ofrecen posibilidades de abstracción que
incluyen:
Control avanzado de estructuras. Procedimientos de alto nivel, declaración de
funciones. Tipos de datos que incluyen estructuras, registros, uniones, clases y
conjuntos. Sofisticado procesamiento de macros.
Lenguaje [editar]Un programa escrito en lenguaje Assembly consiste en una
serie de instrucciones que corresponden al flujo de órdenes ejecutables que
pueden ser cargadas en la memoria de una computadora. Por ejemplo, un
procesador x86 puede ejecutar la siguiente instrucción binaria como se expresa en
código maquina:
Binario: 10110000 01100001 (Hexadecimal: 0xb061) La representación
equivalente en Assembly es más fácil de recordar:
mov al, 061h Esta instrucción significa:
Mueva el valor hexadecimal 61 (97 decimal) al registro “al”. El mnemónico “mov”
es un código de operación u “opcode” , elegido por los diseñadores de la colección
de instrucciones para abreviar “move” (mover).- El opcode es seguido por una lista
de argumentos o parámetros, completando una instrucción de ensamblador típica.
La transformación del lenguaje Assembly en código máquina la realiza un
programa ensamblador, y la traducción inversa la puede efectuar un
desensamblador. A diferencia de los lenguajes de alto nivel, aquí hay usualmente
una correspondencia 1 a 1 entre las instrucciones simples del ensamblador y el
lenguaje máquina. Sin embargo, en algunos casos, un ensamblador puede
proveer “pseudo instrucciones” que se expanden en un código de máquina más
extenso a fin de proveer la funcionalidad necesaria. Por ejemplo, para un código
máquina condicional como “si X mayor o igual que” , un ensamblador puede
utilizar una pseudoinstrucción al grupo “haga si menor que” , y “si = 0″ sobre el
resultado de la condición anterior. Los ensambladores más completos también
proveen un rico lenguaje de macros que se utiliza para generar código más
complejo y secuencias de datos.
Cada arquitectura de computadoras tiene su propio lenguaje de máquina, y en
consecuencia su propio lenguaje Assembly. Los ordenadores difieren en el tipo y
número de operaciones que soportan; también pueden tener diferente cantidad de
registros, y distinta representación de los tipos de datos en memoria. Aunque la
mayoría de las computadoras son capaces de cumplir esencialmente las mismas
funciones, la forma en que lo hacen difiere, y los respectivos lenguajes Assembly
reflejan tal diferencia.
Pueden existir múltiples conjuntos de mnemónicos o sintáxis de Assembly para un
mismo conjunto de instrucciones, instanciados típicamente en diferentes
programas ensamblador. En estos casos, la alternativa más popular es la provista
por los fabricantes, y usada en los manuales del programa.
Código máquina (o lenguaje de máquina) [editar]El lenguaje de máquina está
formado por instrucciones sencillas, que -dependiendo de la estructura del
procesador- pueden especificar:
Registros específicos para operaciones aritméticas, direccionamiento o control de
funciones. Posiciones de memoria específicas (offset). Modos de direccionamiento
usados para interpretar operandos. Las operaciones más complejas se realizan
combinando estas instrucciones sencillas, que pueden ser ejecutadas
secuencialmente o mediante instrucciones de control de flujo.
Las operaciones disponibles en la mayoría de los conjuntos de instrucciones
incluye:
mover llenar un registro con un valor constante mover datos de una posición de
memoria a un registro o viceversa escribir y leer datos de dispositivos computar
sumar, restar, multiplicar o dividir los valores de dos registros, colocando el
resultado en uno de ellos o en otro registro realizar operaciones binarias,
incluyendo operaciones lógicas (AND/OR/XOR/NOT) comparar valores entre
registros (mayor, menor, igual) afectar el flujo del programa saltar a otra posición
en el programa y ejecutar instrucciones allí saltar si se cumplen ciertas
condiciones (IF) saltar a otra posición, pero guardar el punto de salida para
retornar (CALL, llamada a subrutinas) Algunas computadoras incluyen
instrucciones complejas dentro de sus capacidades. Una sola instrucción compleja
hace lo mismo que en otras computadoras puede requerir una larga serie de
instrucciones, por ejemplo:
salvar varios registros en la pila de una sola vez mover grandes bloques de
memoria operaciones aritméticas complejas o de punto flotante (seno, coseno,
raíz cuadrada ) El nivel de lenguaje Assembly tiene aspectos importantes de los
niveles de microarquitectura, en los cuales se encuentra (ISA y sistema operativo)
estos dos se utilizan para la traducción en lugar de la interpretación. Algunas
características del lenguaje se describen a continuación Los programas que sirven
para traducir algún programa para el usuario se llama traductores, el lenguaje en
que esta escrito el programa original se llama lenguaje fuente, el lenguaje original
que sea modificado se llama lenguaje objeto.
Se usa la traducción cuando se cuenta con un procesador (ya sea hardware o un
interprete) para el lenguaje objeto pero no para el lenguaje fuente, Si la traducción
se realiza correctamente, la ejecución del programa traducido dará exactamente
los mismos resultados que habría dado la ejecución del programa fuente. Hay dos
diferencias entre traducción e interpretación, en la traducción no se ejecuta
directamente el programa original, en el lenguaje fuente se convierte en un
programa equivalente llamado programa objeto o programa binario ejecutable y
este funciona solo cuando se ha acabado la traducción.
El código máquina, un simple patrón de bits, es hecho legible reemplazando
valores crudos por símbolos denominados mnemónicos. Se inventó para facilitar la
tarea de los primeros programadores que hasta ese momento tenían que escribir
directamente en código binario. antes aún era peor, ya que el código de ceros y
unos (el programa) debía introducirse en una tarjeta perforada. La posición
ocupada por cada punto equivalía a un “1″ o a un “0″ según hubiera un hueco o
no. Lo cual suponía una forma casi idéntica en la que hoy se escriben los datos
binaros en soportes tales como los CDs y DVDs.
Mientras que una computadora reconoce la instrucción de máquina IA-32
10110000 01100001
para los programadores de microprocesadores x86 es mucho más fácil reconocer
dicha instrucción empleando lenguaje Assembly:
movb 0×61,%al
(que significa mover el valor hexadecimal 61 (97 decimal) al registro ‘al’.)
Cada instrucción de la máquina se transforma en una única instrucción en código
simbólico.
Pero además, para mejorar la legibilidad del programa, el código simbólico
introduce instrucciones adicionales, que no corresponden a ninguna instrucción de
la máquina y que proporcionan información. Se llaman “pseudoinstrucciones”.
El código simbólico puede parecer de difícil acceso, pero es más fácil de recordar
e interpretar que el binario o el hexadecimal.
Los lenguajes simbólicos no resuelven definitivamente el problema de cómo
programar un ordenador de la manera más sencilla posible. Para utilizarlos, hay
que conocer a fondo el microprocesador, los registros de trabajo de que dispone,
la estructura de la memoria, y muchas cosas más.
Además, el lenguaje Assembly está demasiado ligado al microprocesador para
que sea posible escribir programas independientes de la máquina en que van a
ejecutarse.
Este código simbólico no puede ser ejecutado directamente por un ordenador, por
lo que es preciso traducirlo previamente. Pero la traducción es un proceso
mecánico y repetitivo, que se presta a su realización por un programa de
ordenador.
Los programas que traducen código simbólico al lenguaje de máquina se llaman
ensambladores (“assembler”, en inglés), porque son capaces de ensamblar el
programa traducido a partir de varias piezas, procedimientos o subrutinas a código
binario (“1″ y “0″) que entiende el procesador.
Ejemplos de lenguaje Assembly [editar]
Ejemplo 1 [editar]El siguiente es un ejemplo del programa clásico Hola mundo
escrito para la arquitectura de procesador x86 (bajo el sistema operativo DOS ).
.model small .stack .data Cadena1 DB ‘Hola Mundo.$’ .code
programa:
mov ax, @data
mov ds, ax
mov dx, offset Cadena1
mov ah, 9
int 21h
end programa
8.2.2 Almacenamiento Lenguaje Ensamblador
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.
Además nos permite acceder directamente a los recursos de la máquina para un
mejor desempeño.
8.3 Registros Lenguaje Ensamblador
Los registros del procesador se emplean para controlar instrucciones en ejecución,
manejar direccionamiento de memoria y proporcionar capacidad aritmética. Los
registros son espacios físicos dentro del microprocesador con capacidad de 4 bits
hasta 64 bits dependiendo del microprocesador que se emplee. Los registros son
direccionables por medio de una viñeta, que es una dirección de memoria. Los
bits, por conveniencia, se numeran de derecha a izquierda (15, 14, 13…. 3, 2, 1,
0), los registros están divididos en seis grupos los cuales tienen un fin específico.
Los registros se dividen en:
• Registros de segmento
• Registros de apuntadores de instrucciones
• Registros apuntadores
• Registros de propósitos generales
• Registro índice
• Registro de bandera.
Registros de uso general
AX = Registro acumulador, dividido en AH y AL (8 bits cada uno).- Interviene en
las operaciones aritméticas y lógicas, después de la operación arroja un resultado.
BX = Registro base, dividido en BH y BL.- Se utiliza en transferencias de datos
entre la memoria y el procesador.
CX = Registro contador, dividido en CH y CL.- Se utiliza como contador en
bucles(LOOP), en operaciones con cadenas(REP), y en desplazamientos(CL).
DX = Registro de datos, dividido en DH y DL.- Se utiliza en operaciones de
multiplicación y división junto con Ax y en operaciones de entrada y salida de
puertos, su mitad inferior DL contiene el número de puertos.
Registros de segmento.
Un registro de segmento se utiliza para alinear en un limite de párrafo o dicho de
otra forma codifica la dirección de inicio de cada segmento y su dirección en un
registro de segmento supone cuatro bits 0 a su derecha.
Un registro de segmento tiene 16 bits de longitud y facilita un área de memoria
para direccionamientos conocidos como el segmento actual. Los registros de
segmento son: CS (código), DS (datos), SS (pila), ES , FS y GS.
Registro Apuntador de instrucciones.(IP)
El registro apuntador de instrucciones (IP) de 16 bits contiene el desplazamiento
de dirección de la siguiente instrucción que se ejecuta.
Registro índice.
Los registros SI y DI están disponibles para direccionamientos indexados y para
sumas y restas. Que son las operaciones de punta.
Registro de bandera.
Los registros de banderas sirven parar indicar el estado actual de la maquina y el
resultado del procesamiento, Cuando algunas instrucciones piden comparaciones
o cálculos aritméticos cambian el estado de las banderas.
8.3.1 Distribución Lenguaje Ensamblador
La distribución es el proceso en el que el programa generado puede ejecutarse en
otras máquinas. Con respecto al ensamblador, la mayoría del direccionamiento se
hace relativo para que el programa sea relocalizable por un programa llamado
cargador. En el caso de programas compiladores se necesitan de las librerías, si
son estáticos se incluyen en el ejecutable por lo que el programa se hace gráfico,
si son dinámicas no pero el programa es más pequeño. Debido a la complejidad
del software actual se necesitan de asistentes para poder instalar y ejecutar un
programa.
8.3.2 Asignación Lenguaje Ensamblador
8.4 Administración Memoria Lenguaje Ensamblador
La administración de la memoria es un proceso hoy en día muy importante, de tal
modo que su mal o buen uso tiene una acción directa sobre el desempeño de
memoria.
• En general un ensamblador tiene un administrador de memoria más limitado que
un compilador.
En la mayoría de los lenguajes de programación el uso de punteros no estaba
vigilado por lo que se tienen muchos problemas con el uso de memoria.
• Los lenguajes más recientes controlan el uso de punteros y tienen un programa
denominado recolector de basura que se encarga de limpiar la memoria no
utilizada mejorando el desempeño.
Orlando Crescencio Cosgaya Aguilar.
Documentos relacionados
Descargar