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.