manual completo para descargar

93
PROGRAMACIÓN DE SISTEMAS I UNIDAD I INTRODUCCION A LA PROGRAMACION DE SISTEMAS 1.1 ¿QUÉ ES Y QUE ESTUDIA LA PROGRAMACIÓN DE SISTEMAS? PROGRAMACIÓN DE SISTEMAS: Conjunto de reglas para crear soluciones a problemas computables. Conjunto de herramientas que nos permiten crear software de base que son de utilidad para interactuar con la máquina. SOFTWARE DE BASE: Compilador, Querys, Sistema Operativo, Cargador. AUTÓMATA: Son las cadenas posibles que aceptan un lenguaje. EXPRESIONES REGULARES: Conjunto de símbolos que aceptan una palabra reservada. GRAMÁTICA: Reglas para escribir las sentencias del lenguaje. 1.2 ¿HERRAMIENTAS DESARROLLADAS CON LA TEORÍA DE LA PROGRAMACIÓN DE SISTEMAS? Desarrolla software de base como: Traductores Cargadores Ligadores Herramientas Utilerías DBMS Generadores de código 1.3 LENGUAJES LENGUAJE DE PROGRAMACIÓN: Es la notación formal para la descripción de algoritmos, basada en un conjunto de instrucciones en alto nivel, que

Upload: eli-de-rivera

Post on 02-Aug-2015

202 views

Category:

Documents


3 download

TRANSCRIPT

Page 1: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

IUNIDAD I

INTRODUCCION A LA PROGRAMACION DE SISTEMAS

1.1 ¿QUÉ ES Y QUE ESTUDIA LA PROGRAMACIÓN DE SISTEMAS?

PROGRAMACIÓN DE SISTEMAS: Conjunto de reglas para crear soluciones a problemas computables. Conjunto de herramientas que nos permiten crear software de base que son de utilidad para interactuar con la máquina.

SOFTWARE DE BASE: Compilador, Querys, Sistema Operativo, Cargador.

AUTÓMATA: Son las cadenas posibles que aceptan un lenguaje.

EXPRESIONES REGULARES: Conjunto de símbolos que aceptan una palabra reservada.

GRAMÁTICA: Reglas para escribir las sentencias del lenguaje.

1.2 ¿HERRAMIENTAS DESARROLLADAS CON LA TEORÍA DE LA PROGRAMACIÓN DE SISTEMAS?

Desarrolla software de base como: TraductoresCargadoresLigadoresHerramientasUtileríasDBMSGeneradores de código

1.3 LENGUAJES

LENGUAJE DE PROGRAMACIÓN:

Es la notación formal para la descripción de algoritmos, basada en un conjunto de instrucciones en alto nivel, que finalmente pasarán a bajo nivel para interactuar con el hardware y generar herramientas de trabajo.Los lenguajes son sistemas de comunicación. Un lenguaje de programación consiste en todos los símbolos, caracteres y reglas de uso que permiten a las personas "comunicarse" con las computadoras. Existen por lo menos varios cientos de lenguajes y dialectos de programación diferentes. Algunos se crean para una aplicación especial, mientras que otros son herramientas de uso

Page 2: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

general más flexibles que son apropiadas para muchos tipos de aplicaciones. En todo caso los lenguajes de programación deben tener instrucciones que pertenecen a las categorías ya familiares de entrada/salida, cálculo/manipulación de textos, lógica/comparación y almacenamiento/recuperación.

EJEMPLO DE SÍMBOLOS QUE COMPONEN UN PROGRAMA:

LENGUAJE

PASCAL

DELIMITADORES

OPERACIONALES

+,-,*,/,MOD

PALABRAS RESERVADAS

:=ASIGNACION

RELACIONALES

==<>><

ORNOTAND

LOGICOS

ARITMETICOS

IDENTIFICADORES

SIMBOLOS QUE RECHAZA EL LENGUAJE

FUNCIONES

SIMBOLOS ESPECUALES

DECLARACION DE CONSTANTESDECLARACION DE VARIABLESCICLOS

LENGUAJE

PASCAL

DELIMITADORES

OPERACIONALES

+,-,*,/,MOD

PALABRAS RESERVADAS

:=ASIGNACION

RELACIONALES

==<>><

ORNOTAND

LOGICOS

ARITMETICOS

IDENTIFICADORES

SIMBOLOS QUE RECHAZA EL LENGUAJE

FUNCIONES

SIMBOLOS ESPECUALES

DECLARACION DE CONSTANTESDECLARACION DE VARIABLESCICLOS

No obstante, aunque todos los lenguajes de programación tienen un conjunto de instrucciones que permiten realizar dichas operaciones, existe una marcada diferencia en los símbolos, caracteres y sintaxis de los lenguajes de máquina, lenguajes ensambladores y lenguajes de alto nivel.

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.

Page 3: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

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(polisemantica). Dificultad o imposibilidad de una formalización completa.

1.3.2 LENGUAJES ARTIFICIALES

LENGUAJES DE INTELIGENCIA ARTIFICIAL

Podemos distinguir tres grandes estilos o subfamilias de los lenguajes de inteligencia artificial. Los tres estilos de programación son los siguientes: programación funcional, programación relacional y programación por objetos. El lenguaje más representativo del estilo funcional es el LISP, LOGO por su identificación con LSIP, cae de lleno dentro de este estilo. El lenguaje más representativo del estilo relacional PROLOG. El lenguaje más representativo del estilo de programación por objetos es el SMALLTALK, pero existen varios dialectos de LISP que permite programar en esta forma.

1.3.3 PROCESO DE COMUNICACIÓN

Page 4: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

1.4 TRADUCTOR Y SU ESTRUCTURA

Un traductor se define como un programa que traduce o convierte desde un texto o programa escrito en un lenguaje fuente hasta un texto o programa equivalente escrito en un lenguaje destino produciendo, si cabe, mensajes de error. Los traductores engloban tanto a los compiladores (en los que el lenguaje destino suele ser código máquina) como a los intérpretes (en los que el lenguaje destino está constituido por las acciones atómicas que puede ejecutar el intérprete).

Es importante destacar la velocidad con la que hoy en día se puede construir un compilador. En la década de 1950, se consideró a los traductores como programas notablemente difíciles de escribir. El primer compilador de Fortran (Formula Translator), por ejemplo, necesitó para su implementación el equivalente a 18 años de trabajo individual (realmente no se tardó tanto puesto que el trabajo se desarrolló en equipo). Hasta que la teoría de autómatas y lenguajes formales no se aplicó a la creación de traductores, su desarrollo ha estado plagado de problemas y errores. Sin embargo, hoy día un compilador básico puede ser el proyecto fin de carrera de cualquier estudiante universitario de Informática.

Estructura:Un traductor divide su labor en dos etapas: una que analiza la entrada y genera estructuras intermedias y otra que sintetiza la salida a partir de dichas estructuras. Por tanto, el esquema de un traductor pasa de ser el de la anterior, a ser el de la siguiente figura:

TRADUCTOR

Programa de Entrada escrito en Lenguaje Fuente

Programa de Salida escrito en Lenguaje Destino

Mensaje de Error

ESQUEMA PRELIMINAR DE TRADUCTOR

Page 5: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Básicamente los objetivos de la etapa de análisis son: a) controlar la corrección del programa fuente, y b) generar las estructuras necesarias para comenzar la etapa de síntesis.Para llevar esto a cabo, la etapa de análisis consta de las siguientes fases:Análisis lexicográfico. Divide el programa fuente en los componentes básicos del lenguaje a compilar. Cada componente básico es una subsecuencia de caracteres del programa fuente, y pertenece a una categoría gramatical: números, identificadores de usuario (variables, constantes, tipos, nombres de procedimientos, palabras reservadas, signos de puntuación, etc).

Análisis sintáctico. Comprueba que la estructura de los componentes básicos sea correcta según las reglas gramaticales del lenguaje que se compila.

Análisis semántico. Comprueba que el programa fuente respeta las directrices del lenguaje que se compila (todo lo relacionado con el significado): chequeo de tipos, rangos de valores, existencia de variables, etc.

Cualquiera de estas tres fases puede emitir mensajes de error derivados de fallos cometidos por el programador en la redacción de los textos fuente. Mientras más errores controle un compilador, menos problemas dará un programa en tiempo de ejecución. Por ejemplo, el lenguaje C no controla los límites de un array, lo que provoca que en tiempo de ejecución puedan producirse comportamientos del programa de difícil explicación.La etapa de síntesis construye el programa objeto deseado (equivalente semánticamente al fuente) a partir de las estructuras generadas por la etapa de análisis. Para ello se compone de tres fases fundamentales:

Generación de código intermedio. Genera un código independiente de la máquina muy parecido al ensamblador. No se genera código máquina directamente porque así es más fácil hacer pseudocompiladores y además se facilita la optimización de código independientemente del microprocesador.Generación del código máquina. Crea un bloque de código máquina ejecutable, así como los bloques necesarios destinados a contener los datos.

ANALISISFuente Destino

Error en el ProgramaFuente

ESQUEMA POR ETAPAS DE UN TRADUCTOR

SINTESIS

Error al GenerarCódigo

Page 6: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Fase de optimización. La optimización puede realizarse sobre el código intermedio (de forma independiente de las características concretas del microprocesador), sobre el código máquina, o sobre ambos. Y puede ser una aislada de las dos anteriores, o estar integrada con ellas.

1.4.1 ENSAMBLADORES

¿Que es ensamblador y para que sirve?

Cuando se empezaron a utilizar símbolos nemotécnicos, se escribieron programas para traducir automáticamente los programas escritos en lenguaje ensamblador a lenguaje máquina. A estos programas traductores se les llamo ensambladores.La entrada para un ensamblador es un programa fuente escrito en lenguaje ensamblador. La salida es un programa objeto, escrito en lenguaje de máquina. El programa objeto incluye también la información necesaria para que el cargador pueda preparar el programa objeto para su ejecución.Para evitar confusiones, de aquí en adelante llamaremos lenguaje ensamblador al conjunto de nemotécnicos y a las reglas para su manejo. Al programa que traduce un programa objeto a partir de un programa escrito en lenguaje ensamblador lo llamaremos ensamblador.

Motivos para utilizarlo:

RapidezMayor control de la computadoraIndependencia del lenguajeLa mayoría de las computadoras pueden ensamblarloMotivo para no utilizarlo:

Dependencia de hardwareMayor tiempo de codificación

Comprensión más profunda de la computadoraErrores más frecuentes en el programa

Tipos de Ensambladores

Aunque todos los ensambladores realizan básicamente las mismas tareas, podemos clasificarlos de acuerdo a características.

Ensambladores Cruzados (Cross-Assembler). Se denominan así los ensambladores que se utilizan en una computadora que posee un procesador diferente al que tendrán las computadoras donde va a ejecutarse el programa objeto producido.

Page 7: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

El empleo de este tipo de traductores permite aprovechar el soporte de medios físicos (discos, impresoras, pantallas, etc.), y de programación que ofrecen las máquinas potentes para desarrollar programas que luego los van a ejecutar sistemas muy especializados en determinados tipos de tareas.

Ensambladores Residentes. Son aquellos que permanecen en la memoria principal de la computadora y cargan, para su ejecución, al programa objeto producido. Este tipo de ensamblador tiene la ventaja de que se puede comprobar inmediatamente el programa sin necesidad de transportarlo de un lugar a otro, como se hacía en cross-assembler, y sin necesidad de programas simuladores.Sin embargo, puede presentar problemas de espacio de memoria, ya que el traductor ocupa espacio que no puede ser utilizado por el programador.

Macroensambladores.

Son ensambladores que permiten el uso de macroinstrucciones (macros). Debido a su potencia, normalmente son programas robustos que no permanecen en memoria una vez generado el programa objeto. Puede variar la complejidad de los mismos, dependiendo de las posibilidades de definición y manipulación de las macroinstrucciones, pero normalmente son programas bastantes complejos, por lo que suelen ser ensambladores residentes.

Microensambladores.

Generalmente, los procesadores utilizados en las computadoras tienen un repertorio fijo de instrucciones, es decir, que el intérprete de las mismas interpretaba de igual forma un determinado código de operación. El programa que indica al intérprete de instrucciones de la UCP cómo debe actuar se denomina microprograma. El programa que ayuda a realizar este microprograma se llama microensamblador. Existen procesadores que permiten la modificación de sus microprogramas, para lo cual se utilizan microensambladores.

Ensambladores de una fase.

Estos ensambladores leen una línea del programa fuente y la traducen directamente para producir una instrucción en lenguaje máquina o la ejecuta si se trata de una pseudoinstrucción. También va construyendo la tabla de símbolos a medida que van apareciendo las definiciones de variables, etiquetas, etc.Debido a su forma de traducción, estos ensambladores obligan a definir los símbolos antes de ser empleados para que, cuando aparezca una referencia a un determinado símbolo en una instrucción, se conozca la dirección de dicho símbolo y se pueda

Page 8: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

traducir de forma correcta. Estos ensambladores son sencillos, baratos y ocupan poco espacio, pero tiene el inconveniente indicado.

Ensambladores de dos fases.

Los ensambladores de dos fases se denominan así debido a que realizan la traducción en dos etapas. En la primera fase, leen el programa fuente y construyen una tabla de símbolos; de esta manera, en la segunda fase, vuelven a leer el programa fuente y pueden ir traduciendo totalmente, puesto que conocen la totalidad de los símbolos utilizados y las posiciones que se les ha asignado.

1.4.2 COMPILADORES

Hoy en día, un compilador es un traductor que facilita la comunicación entre el programador y la máquina, por medio de un proceso de transformación.

Un compilador es un programa que lee las líneas escritas en un lenguaje de programación (como Pascal) y las traduce a otro que pueda ejecutar la computadora. Los programas compilados se ejecutan más rápido que los interpretados, debido a que han sido completamente traducidos a lenguaje de máquina y no necesitan compartir memoria con el intérprete.

A grandes rasgos un compilador es un programa que lee un programa escrito es un lenguaje, el lenguaje fuente, y lo traduce a un programa equivalente en otro lenguaje, el lenguaje objeto. Como parte importante de este proceso de traducción, el compilador informa a su usuario de la presencia de errores en el programa fuente.

El programa compilador traduce las instrucciones en un lenguaje de alto nivel a instrucciones que la computadora puede interpretar y ejecutar. Para cada lenguaje de programación se requiere un compilador separado. El compilador traduce todo el programa antes de ejecutarlo. Los compiladores son, pues, programas de traducción insertados en la memoria por el sistema operativo para convertir programas de cómputo en pulsaciones electrónicas ejecutables (lenguaje de máquina).

Estructura de un Compilador:

La estructura de un compilador, esta dividida en cuatro grandes módulos, cada uno independiente del otro, se podría decir que un compilador esta formado por cuatros módulos mas a su vez. El primero de ellos es el preprocesador, es el encargado de

Page 9: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

transformar el código fuente de entrada original en el código fuente puro.

El segundo modulo es el de compilación que recibe el código fuente puro, este es él modulo principal de un compilador, pues si ocurriera algún error en esta etapa el compilador no podría avanzar. En esta etapa se somete al código fuente puro de entrada a un análisis léxico gráfico, a un análisis sintáctico, a un análisis semántico, que construyen la tabla de símbolos, se genera un código intermedio al cual se optimiza para así poder producir un código de salida generalmente en algún lenguaje ensamblador.

El tercer modulo es el llamado modulo de ensamblado, este modulo no es ni más mi menos que otro compilador pues recibe un código fuente de entrada escrito en ensamblador, y produce otro código de salida, llamado código binario no enlazado.

El cuarto y ultimo modulo es el encargado de realizar el enlazado del código de fuente de entrada (código maquina relocalizable) con las librerías que necesita, como así también de proveer al código de las rutinas necesarias para poder ejecutarse y cargarse a la hora de llamarlo para su ejecución, modifica las direcciones relocalizables y ubica los datos en las posiciones apropiadas de la memoria.

Tipos de Compiladores:Una sola pasada: Examina el código fuente una vez, generando el código o programa objeto. Pasadas múltiples: Requieren pasos intermedios para producir un código en otro lenguaje, y una pasada final para producir y optimizar el código producido durante los pasos anteriores. Optimación: Lee un código fuente, lo analiza y descubre errores potenciales sin ejecutar el programa. Compiladores incrementales: generan un código objeto instrucción por instrucción (en vez de hacerlo para todo el programa) cuando el usuario teclea cada orden individual. El otro tipo de compiladores requiere que todos los enunciados o instrucciones se compilen conjuntamente. Ensamblador: El lenguaje fuente es lenguaje ensamblador y posee una estructura sencilla. Compilador cruzado: se genera código en lenguaje objeto para una máquina diferente de la que se está utilizando para compilar. Es perfectamente normal construir un compilador de Pascal que genere código para MS-DOS y que el compilador funcione en Linux y se haya escrito en C++. Compilador con montador: compilador que compila distintos módulos de forma independiente y después es capaz de enlazarlos. Autocompilador: Compilador que está escrito en el mismo lenguaje que va a compilar. Evidentemente, no se puede ejecutar la primera vez. Sirve para hacer ampliaciones al lenguaje, mejorar el código generado, etc.

Page 10: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

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 son generadores de analizadores léxicos y sintácticos. desarrollados para UNIX. Los inconvenientes que tienen son que los analizadores que generan no son muy eficientes. Descompilador: Es un programa que acepta como entrada código máquina y lo traduce a un lenguaje de alto nivel, realizando el proceso inverso a la compilación.

1.4.3 INTERPRETES

Es como un compilador, solo que la salida es una ejecución. El programa de entrada se reconoce y ejecuta a la vez. No se produce un resultado físico (código máquina) sino lógico (una ejecución). Hay lenguajes que sólo pueden ser interpretados, como p.ej. SNOBOL (StriNg Oriented SimBOlyc Language), LISP (LISt Processing), algunas versiones de BASIC (Beginner’s All-purpose Symbolic Instruction Code), etc.

Su principal ventaja es que permiten una fácil depuración. Entre los inconvenientes podemos citar, en primer lugar, la lentitud de ejecución , ya que al ejecutar a la vez que se traduce no puede aplicarse un alto grado de optimización; por ejemplo, si el programa entra en un bucle y la optimización no está muy afinada, las mismas instrucciones se interpretarán y ejecutarán una y otra vez, dilatando la ejecución del programa. Otro inconveniente es que durante la ejecución, el intérprete debe residir en memoria, por lo que consumen más recursos.

Además de que la traducción optimiza el programa acercándolo a la máquina, los lenguajes interpretados tienen la característica de que permiten construir programas que se pueden modificar a sí mismos.

Esquema de traducción/ejecución de un programa interpretado

TRADUCTOR MOTOR DE EJECUCION

PseudoejecutableFuente Ejecución

Page 11: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

1.5 GENERADORES DE CÓDIGO PARA COMPILADORES (COMPILADOR DE COMPILADORES, HERRAMIENTAS)

Estructura del proceso de Compilación:

Analizando en detalle el proceso de compilación, se divide en dos grandes fases, una de Análisis y la otra de Síntesis.

Fase de Análisis:En el llamado análisis lexicográfico o léxico, el compilador revisa y controla que las "palabras" estén bien escritas y pertenezcan a algún tipo de token (cadena) definido dentro del lenguaje, como por ejemplo que sea algún tipo de palabra reservada, o si es el nombre de una variable que este escrita de acuerdo a las pautas de definición del lenguaje. En esta etapa se crea la tabla de símbolos, la cual contiene las variables y el tipo de dato al que pertenece, las constantes literales, el nombre de funciones y los argumentos que reciben etc.

En el análisis sintáctico como su nombre lo indica se encarga de revisar que los tokens estén ubicados y agrupados de acuerdo a la definición del lenguaje. Dicho de otra manera, que los tokens pertenezcan a frases gramaticales validas, que el compilador utiliza para sintetizar la salida. Por lo general las frases gramaticales son representadas por estructuras jerárquicas, por medio de árboles de análisis sintáctico. En esta etapa se completa la tabla de símbolos con la dimensión de los identificadores y los atributos necesarios etc.

El análisis semántico se encarga de revisar que cada agrupación o conjunto de token tenga sentido, y no sea un absurdo. En esta etapa se reúne la información sobre los tipos para la fase posterior, en esta etapa se utiliza la estructura jerárquica de la etapa anterior y así poder determinar los operadores, y operandos de expresiones y preposiciones.

Estructura del proceso de Compilación:

Fase de Síntesis:

Etapa de generación de código intermedio, aunque algunos compiladores no la tienen, es bueno saber de su existencia, en esta etapa se lleva el código del programa fuente a un código interno para poder trabajar mas fácilmente sobre él. Esta representación interna debe tener dos propiedades, primero debe ser fácil de representar y segundo debe ser fácil de traducir al código objeto.

Page 12: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

En la etapa de optimización de código, se busca obtener el código mas corto y rápido posible, utilizando distintos algoritmos de optimización.

Etapa de generación de código, se lleva el código intermedio final a código maquina o código objeto, que por lo general consiste en un código maquina relocalizable o código ensamblador. Se selecciona las posiciones de memoria para los datos (variables) y se traduce cada una de las instrucciones intermedias a una secuencia de instrucciones de maquina puro.

La tabla de símbolos no es una etapa del proceso de compilación, sino que una tarea, una función que debe realizar el proceso de compilación. En ella se almacenan los identificadores que aparecen en el código fuente puro, como así también los atributos de los mismos, su tipo, su ámbito y en el caso de los procedimientos el número de argumentos el tipo de los mismos etc. En otras palabras una tabla de símbolos es una estructura de datos, que contiene un registro por cada identificador, y sus atributos. La tabla de símbolo es accedida tanto para escritura como parar lectura por todas las etapas. Detector de errores o manejador de errores, al igual que la tabla de símbolos no es una etapa del proceso de compilación, si no que es una función, muy importante, pues al ocurrir un error esta función debe tratar de alguna forma el error para así seguir con el proceso de compilación (la mayoría de errores son detectados en las etapas de análisis léxico, análisis sintáctico, análisis semántico).

UNIDAD II

INTRODUCCIÓN AL DISEÑO DE LOS LEGUAJES DE PROGRAMACIÓN

2.1 Visión del Problema

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 propuestos 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

Page 13: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

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 refieren 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

Page 14: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

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. Asumiendo la decisión que se ha hecho de que ninguno de los enfoques anteriores seria suficiente, el próximo punto interesante es: ¿Cuál es el propósito de un lenguaje? Un lenguaje es diseñado a menudo para su aplicación en un área específica. La mayor atención es dada a restringir el área de la aplicación del lenguaje, el mejor lenguaje será para problemas en esa

Page 15: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

área. De hecho no es conveniente el procurar diseñar un lenguaje de uso general conveniente para ningún problema. Todo intento ha sido desilusionando (notablemente PL/YO y ALGOL 68). Actualmente, toda evidencia indica que nadie sabe cómo hacer un trabajo apropiado de diseñar un idioma que será “bueno para todo”. Finalmente, la relación de un nuevo lenguaje hacia lenguajes existentes se debe considerar. Weinberg (1971) discute el fenómeno psicológico de “la inhibición,” que ocurre cuando un viejo lenguaje y un lenguaje nuevo no son semejantes pero idénticos. El usuario es susceptible a confundirse gravemente con la incertidumbre acerca de cuánto del viejo lenguaje conservara en el nuevo. Por ejemplo. Los programadores de FORTRAN que aprende que PL/YO formatear E/S tiene gran problema con E- y formatos DE TIPO F, que no son semejantes pero idéntico a la construcción del FORTRAN. En el resumen es preferible hacer el nuevo lenguaje claramente diferente que hacerlo semejante a algún lenguaje existente. Si los nuevos y los viejos lenguajes Los lenguajes de programación existentes pueden ser la fuente para mejores ideas para la programación de diseñadores de lenguaje. Los diseñadores deben ser muy cuidadosos inclusive acerca de ideas tales en su propio producto, sin embargo, porque los diseñadores del pasado han hecho grandes errores en el pasado. Algunos principios despreciables se pueden pronunciar para distinguir buenas ideas dignas de la perpetuación y de malas ideas dignas sólo de la extinción. ¿Quizás el principal principio deberá preguntar “por qué se hizo esa manera?” Una vez que usted obtiene una respuesta a esta pregunta, se pregunta “Es esa razón (todavía) válida” a menudo la respuesta a esta pregunta será no. Para el ejemplo FORTRAN las extrañas restricciones en subíndices de serie dirigiendo las características de su hardware y tuvo miedo que esto no se podría hacer si ninguna expresión permitiéndolo como un subíndice. Aunque esto se pueda considerar quizás razonable (u optar menos entendible) en la circunstancia que lo ciertamente no es hoy defendible, acerca de la reducción grave en valor práctico resultante. Aunque vale la pena para recordar el que incluso si el juicio general en un lenguaje sea (un diseño malo” esto no significa que identificación no oculta las características que valen la pena en algún lugar mas profundamente. Por ejemplo, aunque APL se pueda criticar en muchas veces, bien puede valer la pena el copiar sus poderosos operadores. Semejantemente, el hecho que una características está comúnmente disponible no puede implicar que sistema operativo una idea buena. Muchos idiomas han seguido ALGOL 60’s principal en permitir el

Page 16: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

tamaño de series para ser decidido en corre tiempo, una característica que introduce los problemas considerables de la implementación e interviene con verificar de error de compila-tiempo. Esta característica puede ser del valor sólo limitado en ciertas áreas de aplicaciones. El fenómeno de declaraciones predefinidas, heredado de FORTRAN, es otro ejemplo de palabrotas diseña. Esta característica en el detalle ilustra la grasa que algún arte actualmente popular de características de hecho totalmente perjudicial programar la calidad. Semejantemente, el hecho de que una de las características están comúnmente disponibles no se puede implicar que sea una buena idea su sistema operativo. Muchos lenguajes han seguido ALGOL 60’s principalmente el permitir el tamaño de series para ser decididazas con mayor rapidez, característica que presenta considerablemente la implementación de problemas e interfiere con el tiempo de compilación del cheque de errores. Esta característica puede ser del valor sólo limitado en ciertas áreas de aplicación. El fenómeno de preferido declaraciones, heredado de FORTRAN, es otro ejemplo de un mal diseño de programación. Esta característica en particular cláusula; entonces probablemente sería irrazonable requerir ELSE en todo IF. La noción relativamente reciente de experimentación con cambios de diseño en condiciones controladas ofrece básicamente los mismos tipos de conclusiones indudablemente la investigación seguirá en las áreas de medir el empleo de los lenguajes de programación y experimentar en el diseño de los lenguaje de programación.

2.2 Consideraciones Preliminares

¿Cual es el propósito del lenguaje?

No hay un lenguaje bueno para todoAplicación específica

Bases de datos, sistemas expertos, cálculo numérico, programación simbólica, diseño algorítmico, etc.¿Es necesario diseñar un nuevo lenguaje?

Ya existe un lenguaje apropiadoEl nuevo lenguaje se diferencia de los existentes

Se consume demasiado tiempo en el diseño e implementación de un nuevo lenguajeEs demasiado fácil diseñar un lenguaje incompletoLenguaje demasiado especializadoSacrificar características del lenguaje por un compilador simple.

Page 17: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Otras opciones:

Un modulo o librería de funcionesAmpliar un lenguaje de programación

2.3 OBJETIVOS Y FILOSOFÍAS DEL DISEÑO DE LOS LENGUAJES DE PROGRAMACIÓN.

• Comunicación humana: Se busca una comunicación eficiente entre el programador y

el ordenador Un buen nivel de comunicación se da cuando los programas son

leíbles No ha de ser necesaria una documentación externa al

programa (minimizar) Es más importante que un programa sea leíble que

escribible Un programa se escribe una vez, pero se lee

muchas durante su depuración, documentación y mantenimiento.

Tendencia actual a separar la interfaz de la implementación de un módulo

La sintaxis ha de reflejar la semántica Reducir las manipulaciones implícitas

• Prevención y detección de errores: Hay que prevenir los errores Hay que facilitar su detección, identificación y corrección

Redundancia Evitar coerciones inductoras de errores

Comprobaciones en tiempo de ejecución

• UsabilidadEfectividad:

Los detalles de implementación no han de oscurecer las intenciones del programador

Soportar abstracción

Modularidad: Separar especificación de implementación

Los Efectos de un cambio han de quedar localizados Evitar los trucos (programas ilegible)

• Compilabilidad Se ha de poder compilar programa en

un tiempo reducido Se ha de poder depurar o aplicar otras

herramientas de análisis

Page 18: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

• Eficiencia: La ejecución ha de ser rápida.• Independencia de la máquina:• Simplicidad:• Uniformidad: Lenguaje predecible• Ortogonalidad: Todas las características del lenguaje se han de poder combinar• Generalización y especialización:

La generalización dice que algo similar también es correcto, pero es difícil de implementar

Hay que especializar para facilitar la implementación sin perder la utilidad del lenguaje

2.4 DISEÑO DETALLADO

En esta etapa se adecua el análisis a las características específicas del ambiente de implementación y se completan las distintas aplicaciones del sistema con los modelos de control, interfaz o comunicaciones, según sea el caso.

MicroestructuraEstructura de las expresionesEstructuras de datosEstructuras de controlEstructura de compilaciónEstructura de la entrada/salida

2.5 CASO DE ESTUDIO.

UNIDAD III

ANÁLISIS LÉXICO

3.1 INTRODUCCIÓN A LOS AUTÓMATAS FINITOS Y EXPRESIONES REGULARES

Page 19: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Los autómatas finitos.

Son las posibles cadenas que acepta un lenguaje. Se compila una expresión regular en un reconocedor construyendo un diagrama de transición que es una instrumentación de un modelo formal denominado autómatas finitos, conocidos también como máquinas de estado finito o (con menos frecuencia en la actualidad) máquinas secuenciales.Es la representación gráfica de las posibles cadenas que acepta un lenguaje dentro de un conjunto de símbolos, se conforma por una quíntupla donde:

Las expresiones regulares.

Son los posibles símbolos que puede aceptar un lenguaje los cuales se pueden representar con un autómata. Conjunto de símbolos para aceptar una palabra reservada.Una expresión regular es una fórmula para denotar "ciertos" lenguajes. Adviértase que decimos lenguaje, no cadena de caracteres. Una expresión regular única denota un conjunto de cadenas, es decir un lenguaje, no una simple cadena. Las expresiones regulares se pueden usar para especificar unidades léxicas presentes en un lenguaje de programación. No todos los lenguajes pueden ser expresados utilizando una expresión regular.Sea un alfabeto å. La expresión regular sobre å y los conjuntos que denotan se definen de manera recursiva como sigue:e es una expresión regular y denota al conjunto vacío {e}. Para cada a Î å, a es una expresión regular y denota al conjunto {a}. Si r y s son expresiones regulares que denotan a los lenguajes R y S respectivamente, entonces tenemos lo siguiente. r+s es una expresión regular que denota a los conjuntos R È S.(r) es una expresión regular que denota al conjunto R.rs es una expresión regular que denota a los conjuntos RS.

r* es una expresión regular que denota al conjunto R*, equivale desde cero a más repeticiones del lenguaje R.r+ es una expresión regular que denota al conjunto R+, equivale a una o más repeticiones del Lenguaje R.ri  es una expresión regular que denota al conjunto Ri, a sí mismo.

Q Es el conjunto de posibles estados Representa al alfabeto

Representa la función de transición y los estados por los que se acepta cada uno de los símbolosqo Estado inicialF Conjunto de estados finales

Page 20: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

3.2 ANÁLIZADOR LÉXICO

El analizador léxico lee los caracteres del programa fuente, y verifica que correspondan a una secuencia lógica (identificador, palabra reservada etc.).Esta secuencia de caracteres recibe el nombre componente léxico o lexema.

FUNCIÓN DEL ANALIZADOR LÉXICO

Su principal función consiste en leer los caracteres de entrada y elabora como salida una secuencia de componentes léxicos (tokens) que utiliza el analizador sintáctico para hacer el análisis.

Es la parte del compilador que lee el texto fuente.

Recibida la orden "obtén el siguiente componente léxico" del analizador sintáctico, el analizador léxico lee los caracteres de entrada hasta que pueda identificar el siguiente componente léxico.

Elimina del programa fuente comentarios, espacios en blanco, caracteres TAB y de línea nueva.

Otra función es relacionar los mensajes de error del compilador con el programa fuente.

En algunos compiladores, el analizador léxico se encarga de hacer una copia del programa fuente en el que están marcados los mensajes de error.

Analizador Léxico dentro de un Compilador

En algunas ocasiones, los analizadores léxicos se dividen en dos fases; la primera llamada "examen", y la segunda, "análisis léxico". El

Page 21: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

examinador se encarga de realizar tareas sencillas, mientras que el analizador léxico es el que realiza las operaciones más complejas.

El objetivo del análisis léxico es gestionar el "bajo nivel" de la entrada (en este caso, texto) y suministrarle al analizador sintáctico la misma ya "filtrada", es decir, conteniendo solamente terminales de la gramática a definir.

La relación entre el analizador léxico y el sintáctico son los tokens y la tabla de símbolos, ya que la función principal del análisis léxico es leer los caracteres de entrada y genera como salida una secuencia de componentes léxicos previamente definidos en expresiones regulares.

ASPECTOS A CONSIDERAR PARA EL ANÁLISIS LÉXICO

Un diseño sencillo de un analizador léxico corresponde a unas reglas del lenguaje meramente sencillas, sin embargo estas son algunas consideraciones importantes:

• Mientras menos complicadas sean las gramáticas, más fácil será realizar un compilador, sólo que previamente se deben considerar las expresiones regulares.

• Hay que considerar si se debe o puede mejorar la eficiencia del compilador.

• También mejorar la transportabilidad del compilador, ya que las letras del alfabeto y los errores propios de los dispositivos pueden limitar al analizador léxico.

El trabajo básico es reconocer los terminales (que podemos llamar ya unidades léxicas), y para ello podemos hacer una tabla con todas las posibilidades que encontraremos en el fichero de la entrada y agruparlos en los tipos necesarios.

3.3 MANEJO DE LOCALIDADES TEMPORALES DE MEMORIA (BUFFERS).

El uso de localidades temporales de memoria es para agilizar el tiempo de compilación, el manejo de este es por medio de estructuras de datos utilizando memoria dinámica, como se vio en materias anteriores como estructura de datos, usando esta implementación de diferentes formas que pueden clasificarse de acuerdo a su argumento de búsqueda.

Page 22: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

3.4 CREACIÓN DE TABLAS DE SÍMBOLOS

TABLAS DE SÍMBOLOSEs una estructura de datos que contiene un registro por cada

token o identificador que define los atributos de ellos mismos. La estructura de datos permite encontrar rápidamente el registro para ser almacenado o consultado por otras fases.

Cuando el análisis léxico detecta un token en el programa fuente, este se introduce en la tabla de símbolos, sin embargo, sus atributos no pueden determinarse durante el análisis léxico.Por ejemplo, dada la siguiente instrucción:Var

inicial, velocidad: integer ;

El tipo entero no se conoce cuando el analizador léxico pasa por toda la instrucción, las fases restantes introducen información sobre los tokens en la tabla y después hace uso de ella.Cuando se hace el análisis semántico y la generación de código intermedio se necesita saber los tipos de cada uno de los identificadores para comprobar si el programa fuente los usa en forma válida y así generar las operaciones apropiadas con ellos.

Page 23: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

3.5 MANEJO DE ERRORES LÉXICOS

Errores LéxicosSon pocos los errores detectables en el analizador léxico porque

se tiene una visión restringida del programa, ya que no se puede distinguir entre un error de escritura de un identificador y una palabra reservada, así como la declaración y utilización de una función, porque todas las consideran en esta fase como un identificador válido y los errores se empiezan a detectar entre el analizador semántico o el analizador sintáctico, según sea el caso.Técnicas de corrección de errores

Existen algunas técnicas de corrección que son utilizadas en compiladores, sin embargo no es conveniente modificar la idea original, ni la lógica de programación del usuario.Las técnicas son las siguientes:

• Modo de pánico. Donde se guardan los caracteres sucesivos de la entrada restante, hasta encontrar un componente léxico válido.

• Borrar un carácter extraño que no haya sido declarado o definido dentro del lenguaje.

• Insertar un carácter que falte.

• Reemplazar un carácter faltante

• Intercambiar caracteres adyacentes

3.6 GENERADORES DE CÓDIGO LÉXICO: LEX, FLEX, JFLEX, JAVACC, ANTLR.

Generador de Código Lex

El lex es un generador de programas diseñado para el proceso léxico de cadenas de caracteres de entrada. El programa acepta una especificación, orientada a resolver un problema de alto nivel para comparar literales de caracteres, y produce un programa C que reconoce expresiones regulares. Estas expresiones las especifica el usuario en las especificaciones fuente que se le dan al lex. El código lex reconoce estas expresiones en una cadena de entrada y divide esta entrada en cadenas de caracteres que coinciden con las expresiones. En los bordes entre los literales, se ejecutan las secciones de programas proporcionados por el usuario. El fichero fuente lex asocia las expresiones regulares y los fragmentos de programas. Puesto que cada expresión aparece en la entrada del programa escrito por el lex, se ejecuta el fragmento correspondiente.

Page 24: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

El usuario proporciona el código adicional necesario para completar estas funciones, incluyendo código escrito por otros generadores. El programa que reconoce las expresiones se genera en forma de fragmentos de programa C del

usuario, El lex no es un lenguaje completo sino un generador que representa una cualidad de un nuevo lenguaje que se añade al leguaje de programación C.

Formato fuente del LexEl formato general de la fuente lex es:

{definiciones}%% /* sección de definiciones*/

{reglas}%% /* sección de reglas*/

{Sección de rutinas auxiliares}

a) Sección de definiciones

La sección de definiciones contiene la siguiente información:

Código C encerrado entre los delimitadores % y %, que se copia literalmente en el fichero de salida lex.yy.c antes de la definición de la función yylex (). Habitualmente, esta sección contiene declaraciones de variables y funciones que se utilizaran posteriormente en la sección de reglas, así como directivas #include.

Definiciones propias de Lex, que permiten asignar nombre a una expresión regular o a una parte de ella, para utilizarlo posteriormente en lugar de una expresión. Para dar nombre a una expresión regular, se escribe el nombre en la primera columna de una línea, seguido por uno o más espacios en blanco y por la expresión regular que representa a los dígitos del 0 al 9 de la siguiente forma:

DIGITO [0-9]

b) Sección de Reglas

La sección de reglas contiene, para cada unidad sintáctica, la expresión regular que la describe, seguida de uno o más espacios en blanco y del código C que debe ejecutarse cuando se localice en la entrada dicha unidad sintáctica. Este código C debe aparecer encerrado entre llaves.

Como ejemplo, consideremos un analizador morfológico que reconozca en la entrada las constantes numéricas y las palabras

Page 25: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

reservadas begin y end. Cada vez que localice una de ellas debe mostrar en la salida un mensaje de aviso de unidad sintáctica reconocida.

c) Sección de rutinas auxiliares

Habitualmente, esta sección contiene las funciones escritas por el usuario para utilizarlas en la sección de reglas, es decir, funciones de soporte. En esta sección tambien se incluyen las funciones de lex que el usuario puede redefinir, como, por ejemplo, la funcion yywrap (). El contenido de esta sección se copia literalmente en el fichero lex.yy.c que genera lex.

Generador de Código JFlex

JFlex es un generador de analizadores lexicográficos desarrollado por Gerwin Klein como extensión a la herramienta JLex desarrollada en la Universidad de Princeton. JFlex está desarrollado en Java y genera código Java.Los programas escritos para JFlex tienen un formato parecido a los escritos en PCLex; de hecho todos los patrones regulares admisibles en Lex también son admitidos por JFlex, por lo que en este apartado nos centraremos tan sólo en las diferencias y extensiones, tanto de patrones como del esqueleto que debe poseer el fichero de entrada a JFlex.

La instalación y ejecución de JFlex es trivial. Una vez descomprimido el fichero dispondremos del fichero JFlex.jar que tan sólo es necesario en tiempo de meta-compilación, siendo el analizador generado totalmente independiente.La clase Main del paquete JFlex es la que se encarga de metacompilar nuestro programa .jflex de entrada; de esta manera, una invocación típica es de la forma:

java JFlex.Main fichero.jflex lo que generará un fichero Yylex.java que implementa al analizador lexicográfico.

Generador de Código Javacc

JavaCC (Java Compiler Compiler - Metacompilador en Java) es el principal metacompilador en JavaCC, tanto por sus posibilidades, como por su ámbito de difusión. Se trata de una herramienta que facilita la construcción de analizadores léxicos y sintácticos por el método de las funciones recursivas, aunque permite una notación relajada muy parecida a la BNF. De esta manera, los analizadores generados utilizan la técnica descendente a la hora de obtener el árbol sintáctico.

Características Generales:

Page 26: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

JavaCC integra en una misma herramienta al analizador lexicográfico y al sintáctico, y el código que genera es independiente de cualquier biblioteca externa, lo que le confiere una interesante propiedad de independencia respecto al entorno. A grandes rasgos, sus principales características son las siguientes:

• Genera analizadores descendentes, permitiendo el uso de gramáticas de propósito general y la la utilización de atributos tanto sintetizados como heredados durante la construcción del árbol sintáctico.

• Las especificaciones léxicas y sintácticas se ubican en un solo archivo. De esta manera la gramática puede ser leída y mantenida más fácilmente. No obstante, cuando se introducen acciones semánticas, recomendamos el uso de ciertos comentarios para mejorar la legibilidad.

• Admite el uso de estados léxicos y la capacidad de agregar acciones léxicas incluyendo un bloque de código Java tras el identificador de un token.

Características Generales:

• Incorpora distintos tipos de tokens: normales (TOKEN), especiales (SPECIAL_TOKEN), espaciadores (SKIP) y de continuación (MORE). Ello permite trabajar con especificaciones más claras, a la vez que permite una mejor gestión de los mensajes de error y advertencia por parte de JavaCC en tiempo de metacompilación.

• Los tokens especiales son ignorados por el analizador generado, pero están disponibles para poder ser procesados por el desarrollador.

• La especificación léxica puede definir tokens de manera tal que no se diferencien las mayúsculas de las minúsculas bien a nivel global, bien en un patrón concreto.• Adopta una notación BNF propia mediante la utilización de símbolos propios de expresiones regulares, tales como (A)*, (A)+.

• Genera por defecto un analizador sintáctico LL(1). No obstante, puede haber porciones de la gramática que no sean LL(1), lo que es resuelto en JavaCC mediante la posibilidad de resolver las ambigüedades desplazar/desplazar localmente en el punto del conflicto. En otras palabras, permite que el a.si. Se transforme en LL(k) sólo en tales puntos, pero se conserva LL(1) en el resto de las reglas mejorando la eficiencia.

• De entre los generadores de analizadores sintácticos descendentes, JavaCC es uno de los que poseen mejor gestión de errores. Los analizadores generados por JavaCC son capaces de localizar

Page 27: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

exactamente la ubicación de los errores, proporcionando información diagnóstica completa.

Generador de Código ANTLR

Antlr permite construir árboles de sintaxis abstracta (ASA) mediante anotaciones en la gramática indicando qué tokens deben tratarse como raíces de subárboles, cuáles son tokens hojas y cuáles deben ignorarse.

Tipo de analizador: Descendente recursivo, LL(k).Código generado: Java, C++, C#.Características adicionales: Construcción de ASTs.

UNIDAD IV

ANALISIS SINTACTICO

4.1 INTRODUCCIÓN A LAS GRAMÁTICAS LIBRES DE CONTEXTO Y ÁRBOLES DE DERIVACIÓN.

A partir de alguna clase de gramáticas se puede construir automáticamente un analizador sintáctico eficiente que determine si un programa fuente esta sintacticamente bien formado. Una gramática diseñada adecuadamente imparte una estructura a un lenguaje de programación útil para la traducción de programas fuente a código objeto correcto y para la detección de errores

Todo lenguaje de programación tiene reglas que prescriben la estructura sintáctica de programas bien formados. La tarea del análisis sintáctico es determinar la estructura sintáctica de un programa a partir de de los tokens producidos por el analizador léxico y, ya sea de manera implícita o explicita, construir un árbol de análisis gramatical o sintáctico que represente esta estructura. De este modo, se puede ver el analizador sintáctico como una función que toma como su entrada la secuencia de tokens producidos por el analizador léxico y que produce como su salida el árbol sintáctico.

Page 28: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

El Analizador Sintáctico dentro del modelo de un compilador.

GRAMÁTICAS Y LENGUAJES SENSIBLES AL CONTEXTO.

Una Gramática Libre de Contexto es un conjunto finito de símbolos o variables que representan categorías aplicables a elementos de léxico. La gramática libre de contexto es muy útil para definir relaciones entre objetos sintácticos tales como la sintaxis de un lenguaje de programación. Una gramática libre de contexto tiene un símbolo de arranque o de objetivo. Son símbolos terminales que representan ejemplos de variables y son reglas de producción que combinan entre sí para el logro del objetivo.

Las gramáticas libres al contexto consta de:

• Los terminales son símbolos básicos con que se forman las cadenas.

• Los no terminales son variables sintácticas que denotan conjuntos de cadenas.

• En una gramática, un no terminal es considerado como el símbolo inicial, y el conjunto de cadenas que representan es el lenguaje definido por la gramática.

• Las producciones de una gramática especifican como se pueden combinar los terminales y los no terminales para formar las cadenas.

Notación BNF

La notación más frecuentemente utilizada para expresar gramáticas libres de contexto es la forma Backus-Naur.Las gramáticas libres de contexto tienen métodos alternativos útiles para desplegar las producciones. Una alternativa que se encuentra con frecuencia es la notación BNF (forma Backus-Naur). Se sabe que los lados izquierdos de todas las producciones en una gramática de tipo 2 son símbolos no terminales únicos. Para cada uno de tales símbolos w, se combina todas las producciones que tienen a w como lado izquierdo. El símbolo w permanece a la izquierda, y todos los

Page 29: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

lados derechos asociados con w son enumerados juntos, separados por el símbolo |. El símbolo relacional se reemplaza por el símbolo ::=.

Por último, los símbolos no terminales, cuando aparezcan, serán encerrados entre paréntesis agudos < >.

Ejemplo:

ÁRBOLES DE DERIVACIONDerivaciones por la izquierda y derecha:

Existen básicamente dos formas de describir como en una cierta gramática una cadena puede ser derivada desde el símbolo inicial. La forma más simple es listar las cadenas de símbolos consecutivas, comenzando por el símbolo inicial y finalizando con la cadena y las reglas que han sido aplicadas. Si introducimos estrategias como reemplazar siempre el no terminal de más a la izquierda primero, entonces la lista de reglas aplicadas es suficiente. A esto se le llama derivación por la izquierda. Por ejemplo, si tomamos la siguiente gramática:

(1) S → S + S (2) S → 1

Y la cadena "1 + 1 + 1", su derivación a la izquierda está en la lista [ (1), (1), (2), (2), (2) ]. Análogamente, la derivación por la derecha se define como la lista que obtenemos si siempre reemplazamos primero el no terminal de más a la derecha. En ese caso, la lista de reglas aplicadas para la derivación de la cadena con la gramática anterior sería la [ (1), (2), (1), (2), (2)].

La distinción entre derivación por la izquierda y por la derecha es importante porque en la mayoría de analizadores la transformación de la entrada es definida dando una parte de código para cada producción que es ejecutada cuando la regla es aplicada. De modo que es importante saber que derivación aplica el analizador, por que determina el orden en el que el código será ejecutado.

Una derivación también puede ser expresada mediante una estructura jerárquica sobre la cadena que está siendo derivada. Por ejemplo, la estructura de la derivación a la izquierda de la cadena "1 + 1 + 1" con la gramática anterior sería:

Page 30: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

S→S+S (1) S→S+S+S (1) S→1+S+S (2) S→1+1+S (2) S→1+1+1 (2) { { { 1 }S + { 1 }S }S + { 1 }S }S

donde { ... }S indica la subcadena reconocida como perteneciente a S. Esta jerarquía también se puede representar mediante un árbol sintáctico:

La derivación por la derecha:

S→ S + S (1) S→ 1 + S (2) S→ 1 + S + S (1) S→ 1 + 1 + S (2) S→ 1 + 1 + 1 (2)

define el siguiente árbol sintáctico:

4.2 DIAGRAMAS DE SINTAXIS

Un diagrama de sintaxis (también llamados diagramas de Conway) es un grafo dirigido donde los elementos no terminales aparecen como rectángulos, y los terminales como círculos o elipses.

Todo diagrama de sintaxis posee un origen y un destino, que no se suelen representar explícitamente, sino que se asume que el origen se encuentra a la izquierda del diagrama y el destino a la derecha.

Cada arco con origen en " y destino en $ representa que el símbolo " puede ir seguido del $ (pudiendo ser " y $ tanto terminales como no terminales). De esta forma todos los posibles caminos desde el inicio del grafo hasta el final, representan formas sentenciales válidas.

Page 31: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Demostraremos que los diagramas de sintaxis permiten representar las mismas gramáticas que la notación BNF, por inducción sobre las operaciones básicas de BNF:

4.3 PRECEDENCIA DE OPERADORES

La precedencia de un operador indica qué tan "cerca" se agrupan dos expresiones. Por ejemplo, en la expresión 1 + 5 * 3, la respuesta es 16 y no 18, ya que el operador de multiplicación ("*") tiene una mayor precedencia que el operador de adición ("+"). Los paréntesis pueden ser usados para marcar la precedencia, si resulta necesario. Por ejemplo: (1 + 5) * 3 evalúa a 18. Si la precedencia de los operadores es la misma, se utiliza una asociación de izquierda a derecha.

La siguiente tabla lista la precedencia de los operadores, con aquellos de mayor precedencia listados al comienzo de la tabla. Los operadores en la misma línea tienen la misma precedencia, en cuyo caso su asociatividad decide el orden para evaluarlos.

Tabla de Precedencia de Operadores

Asociatividad Operadores Información Adicionalno-asociativo new newizquierda [ array()no-asociativos ++ -- incremento/decremento no-asociativos ! ~ - (int) (float) (string) (array) (object) @ tipos izquierda * / % aritmética izquierda + - . aritmética, y cadena izquierda << >> manejo de bits no-asociativos < <= > >= comparación no-asociativos == != === !== comparación

Page 32: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Asociatividad Operadores Información Adicionalizquierda & manejo de bits, y referencias izquierda ^ manejo de bits izquierda | manejo de bits izquierda && lógicos izquierda || lógicos izquierda ? : ternario derecha = += -= *= /= .= %= &= |= ^= <<= >>= asignación izquierda and lógicos izquierda xor lógicos izquierda Or lógicos izquierda , varios usos

Por ejemplo, la siguiente expresión produce un resultado diferente dependiendo de si se realiza la suma o división en primer lugar:

x + y / 100

Si no se indica explícitamente al compilador el orden en que se quiere que se realicen las operaciones, entonces el compilador decide basándose en la precedencia asignada a los operadores. Como el operador de división tiene mayor precedencia que el operador de suma el compilador evaluará y/100 primero.

Así:

x + y / 100

Es equivalente a:

x + (y / 100)

4.4 ANALIZADOR SINTÁCTICO

Tipos de análisis sintáctico

Según la aproximación que se tome para construir el árbol sintáctico se desprenden dos tipos o clases de analizadores:

• Descendentes:Parten del axioma inicial, y van efectuando derivaciones a izquierda hasta obtener la secuencia de derivaciones que reconoce a la sentencia. Pueden ser:

Con retroceso. Con funciones recursivas.

Page 33: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

De gramáticas LL(1).

• Ascendentes:Parten de la sentencia de entrada, y van aplicando derivaciones inversas (desde el consecuente hasta el antecedente), hasta llegar al axioma inicial. Pueden ser:

"Con retroceso. "De gramáticas LR(1).

4.4.1 ANALIZADOR SINTÁCTICO DESCENDENTE (LL)

Analiza una cadena de tokens de entrada mediante búsquedas de los pasos de una derivación por la izquierda. Este método se denomina descendente debido al que el recorrido implicado del árbol de análisis gramatical es un recorrido de preorden y, de este modo, se presenta desde la raíz hacia las hojas. Existen 2 formas: Analizadores sintáctico inverso o en reversa y analizadores sintáctico predictivos.

El analizador sintáctico predictivo intenta predecir la siguiente construcción en la cadena de entrada utilizando uno o mas tokens de búsquedas por adelantado, mientras que un analizador sintáctico inverso intentara las diferentes posibilidades para un análisis sintáctico de la entrada, respaldando un a cantidad arbitraria en la entrada una posibilidad falla, el inverso es mucho mas exacto que el preventivo, pero, su procesamiento es demasiado lento y eso lo hace inadecuado para compiladores prácticos.

Las dos clases de algoritmos de análisis sintáctico descendente se denomina análisis sintáctico descendente recursivo y el análisis sintáctico LL (1).

Por ejemplo considere la gramática:

Y la cadena de entrada w = cad. Para construir un árbol de análisis sintáctico descendente para esta cadena, primero se crea un árbol formado por un solo nodo etiquetado con S. Un apuntador a la entrada apunta a c, el primer símbolo de w.

Ejemplo:

Page 34: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

4.4.2 ANALIZADOR SINTÁCTICO ASCENDENTE (LR)

La “L” indica que la entrada se procesa de izquierda a derecha, y la “R” indica que se produce una derivación por la derecha. Incluirá la construcción de los DFA, así como la construcción de las tablas de análisis sintáctico que se asocian con ellas.

Este analizador utiliza una pila explicita para realizar un análisis sintáctico de manera semejante como lo hace un analizador sintáctico descendente no recursivo. La pila de análisis sintáctico contendrá tanto tokens como no terminales y también alguna información de estado adicional que analizaremos posteriormente. La pila esta vacía al principio de un análisis sintáctico ascendente y al final de un análisis sintáctico exitoso contendrá el símbolo inicial.

Un esquema de análisis sintáctico ascendente es:

Donde la pila de análisis sintáctico esta a la izquierda, la entrada esta en el centro y las acciones del analizador sintáctico están a la derecha.Un analizador sintáctico ascendente tiene dos posibles acciones:1.- DESPLAZAR: o transferir un terminal de la parte frontal de la entrada hasta la parte superior de la pila.

Page 35: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

2.- REDUCIR: una cadena & en la parte superior de la pila a un no terminal A, dada las elecciones BNF A --> &.Por esta razón el analizador sintáctico ascendente se conoce como analizador sintáctico de reducción por desplazamiento.

Diferencias entre los Métodos Ascendente y Descendente

Ambos son métodos que nos sirve para el análisis sintácticos, pero una la mas importante diferencia que hay entre los 2 métodos :

1.- Es en la forma que recorren el árbol gramaticales el descendente lo recorre en forma de preorden de la raíz a las hojas empezando por la izquierda y el ascendente lo recorrerá en forma contraria empezando de las hojas hacia la raíz.

2.- El ascendente utiliza una pila. No es sensible me da chance para aceptar mas cadenas y mas alternativa.

4.5 ADMINISTRACIÓN DE TABLAS DE SÍMBOLOS

Tabla de Símbolos (TDS): Estructura de datos usada por el compilador para asociar a cada símbolo (nombre) del programa una representación de su contenido semántico (atributos).

1. Función análoga a un diccionario.

2. Mecanismo para representar el contexto en un programa.

3. Mantiene la información que se ha recogido acerca de un símbolo desde el momento de su declaración.

4. Mecanismo utilizado en A. Semántico para implementar las restricciones típicas de los lenguajes de programación:

Control de unicidad de identificadores. Asegurar que toda variable fue declarada antes de ser

usada. Verificación de tipos. Implementación de reglas de ámbito (en lenguaje con

estructura de bloques).

TDSs sólo existen durante compilación, no suelen incorporarse al fichero objeto.

1. Se puede incorporar para facilitar depuración (acceso a variables nombre, etc.).

2. También, se incorpora en los interpretes (alternan compilación y ejecución).

Page 36: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

La información guardada (campos) depende del elemento almacenado, del lenguaje y de la estructura del compilador:

Atributos en TDS

1. Nombre del símbolo: string:

2. Dirección en memoria: dirección donde se guardarán los valores de las variables durante la ejecución. No dirección absolutas, sino relativas a una zona de memoria

durante la ejecución. No contiene valores, solo información de almacenamiento.

3. Tipo: codificación correspondiente al tipo de dato asociado al símbolo.

4. Localización (numero línea, nombre fichero): información auxiliar para depuración.

TDS puede estar inicializada con información sobre símbolos especiales del lenguaje (palabras reservadas, funciones de librería, constantes predefinidas, etc...)

Operaciones en TDS (interfaz)

1. Insertar un símbolo en la tabla (comprobando).2. Buscar y recuperar información de un símbolo.3. Modificar información de un símbolo.4. Borrar.5. En lenguajes con estructura de bloques:

NuevoBloque: inicio de un bloque. FinBloque: fin del ámbito de un bloque.

4.6 MANEJO DE ERRORES SINTÁCTICOS Y SU RECUPERACIÓN

A menudo, gran parte de la detección y recuperación de errores en un compilador se centra en la fase de análisis sintáctico. Una razón es que muchos errores son de naturaleza sintáctica o se manifiestan cuando la cadena de componentes léxicos que proviene del analizador léxico desobedece la reglas gramaticales que definen el lenguaje de programación. Otra razón es la precisión de los métodos modernos de análisis sintáctico, que pueden detectar la presencia de errores dentro de los programas de una forma muy eficiente. La detección exacta de la presencia de errores semánticos y lógicos en el momento de la compilación es mucho más difícil.El manejador de errores en un analizador sintáctico tiene objetivos fáciles de establecer:

Page 37: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

• Debe de informar de la presencia de errores con claridad y exactitud.

• Se debe de recuperar de cada error con la suficiente rapidez como para detectar errores posteriores.

No de be retrazar de manera significativa el procesamiento de programas correctos.

Afortunadamente lo errores más comunes son simples y a menudo basta con un mecanismo sencillo de manejo de errores. sin embargo, en algunos casos un error pudo haber ocurrido mucho antes de la posición en que se detectó su presencia, y puede ser muy difícil deducir su naturaleza precisa de error. En los casos difíciles, el manejador de errores quizá tenga que adivinar qué tenía en mente el programador cuando escribió el programa.

¿Cómo debe informar un manejador de errores de la presencia de un error ?, al menos debe informar del lugar en el programa fuente donde se detecta el error, porque es muy probable que el error real se haya producido en alguno de los componentes léxicos anteriores. Una estrategia común empleada por muchos compiladores e imprimir la línea errónea con un apuntador a la posición donde se detecta el error. Si hay una posibilidad razonable de saber cuál es realmente el error, también se incluye un mensaje de diagnóstico informativo y comprensible; por ejemplo "Falta punto y coma en esta posición".

Una vez detectado el error, ¿ Cómo se debe de recuperar el analizador sintáctico ?, En la mayoría de los casos, no es adecuado que el analizador sintáctico abandone después de detectar el primer error, porque el posterior procesamiento de la entrada podría revelar más errores. Normalmente, hay alguna forma de recuperación del error donde el analizador sintáctico intenta volver él mismo a un estado en el que el procesamiento de la entrada pueda continuar con una esperanza razonable de que hará el análisis de la entrada correcta o de que será manejada correctamente por el compilador.Hay muchas estrategias generales distintas que puede emplear un analizador sintáctico para recuperarse de un error sintáctico. Aunque ninguna de ellas ha demostrado ser la aceptación universal, algunos métodos tienen una amplia aplicabilidad. Mencionaremos algunas estrategias, entre las cuales están:

Recuperación en modo de pánico: Este es el método más sencillo de implantar y pueden utilizarlo la mayoría de los métodos de análisis sintáctico. Al descubrir un error, el analizador sintáctico desecha símbolo de entrada, de uno en uno, hasta que encuentra uno perteneciente a un conjunto designado de componente léxicos de sincronización. Estos componente léxicos de sincronización son generalmente delimitadores, como el punto y coma o la palabra clave end, cuyo papel en el programa fuente está claro. Es evidente que

Page 38: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

quien diseña el compilador debe seleccionar los componentes léxicos de sincronización adecuados para el lenguaje fuente. Aunque la corrección en modo de pánico a menudo omite una cantidad considerable de entrada sin comprobar la existencia de errores adicionales, tiene la ventaja de la sencillez y, a diferencia de otros métodos, esta garantizado contra lazos infinitos. En situaciones en donde son raros los errores múltiples en la misma proposición, este método puede resultar bastante adecuado.Recuperación a nivel de frase: Al descubrir un error, el analizador sintáctico puede realizar una corrección local de la entrada restante; es decir, puede sustituir un prefijo de la entrada restante por alguna cadena que permita continuar al analizador sintáctico. Una corrección local típica sería sustituir una coma por un punto y coma, suprimir un punto y coma sobrante e insertar un punto y coma que falta. La elección de la corrección local corresponde al

diseñador del compilador. Por supuesto se debe de tener cuidado de elegir sustituciones que no conduzcan a lazos infinitos.

Producciones de error: Si se tiene una buena idea de los errores comunes que puede encontrarse, se puede aumentar la gramática del lenguaje con producciones que generen las construcciones erróneas. Entonces se usa esta gramática aumentada con las producciones de error para construir el analizador sintáctico. Si el analizador sintáctico usa una producción de error, se pueden generar diagnósticos de error apropiados para indicar la construcción errónea reconocida en la entrada.

Corrección global: Idealmente, sería deseable que un compilador hiciera el mínimo de cambios posibles al procesar una cadena de entrada incorrecta. Existen algoritmos para elegir una secuencia mínima de cambios para obtener una corrección global de menor costo. Dada una cadena de entrada incorrecta x y la gramática G, estos algoritmos encontrarán un árbol de análisis sintáctico para una cadena relacionada y, tal que el número de inserciones, supresiones y modificaciones de componentes léxicos necesarios para transformar x en y sea el mínimo posible. Por desgracia, la implantación de estos métodos es en general demasiado costosa en términos de tiempo y espacio, así que estas técnicas en la actualidad sólo son de interés teórico.

Se debe señalar que un programa correcto más parecido al original puede no ser lo que el programador tenía en mente. Sin embargo, la noción de corrección de costo mínimo proporciona una escala para evaluar las técnicas de recuperación de errores, y se ha usado para encontrar cadenas de sustitución óptimas para recuperación a nivel de frase.

Page 39: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

4.7 GENERADORES DE CÓDIGO PARA ANALIZADORES SINTÁCTICOS: YACC, BISON, BYACC, ANTLR, JAVACC.

UNIDAD V

ANALISIS SEMANTICO

5.1 ANALIZADOR SEMÁNTICO

La fase de análisis semántico de un procesador de lenguaje es aquélla que computa la información adicional necesaria para el procesamiento de un lenguaje, una vez que la estructura sintáctica de un programa haya sido obtenida. Es por tanto la fase posterior a la de análisis sintáctico y la última dentro del proceso de síntesis de un lenguaje de programación.

Sintaxis de un lenguaje de programación es el conjunto de reglas formales que especifican la estructura de los programas pertenecientes a dicho lenguaje. Semántica de un lenguaje de programación es el conjunto de reglas que especifican el significado de cualquier sentencia sintácticamente válida.

Finalmente, el análisis semántico de un procesador de lenguaje es la fase encargada de detectar la validez semántica de las sentencias aceptadas por el analizador sintáctico.

Dado el siguiente ejemplo en código c:Superficie = base * altura / 2;

La sintaxis del lenguaje C indica que las expresiones se pueden formar con un conjunto de operadores y un conjunto de elementos básicos. Entre los operadores, con sintaxis binaria infija, se encuentran la asignación, el producto y la división. Entre los elementos básicos de una expresión existen los identificadores y las constantes enteras sin signo (entre otros).

Su semántica identifica que en el registro asociado al identificador superficie se le va a asociar el valor resultante del producto de los valores asociados a base y altura, divididos por dos (la superficie de un triángulo).

Finalmente, el análisis semántico del procesador de lenguaje, tras haber analizado correctamente que la sintaxis es válida, deberá comprobar que se satisfacen las siguientes condiciones:

Que todos los identificadores que aparecen en la expresión hayan sido declarados en el ámbito actual, o en alguno de sus ámbitos (bloques2) previos.

Page 40: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Que la subexpresión de la izquierda sea semánticamente válida, es decir, que sea un lvalue3.

Que a los tipos de los identificadores base y altura se les pueda aplicar el operador de multiplicación. Un registro en C, por ejemplo, no sería válido.

Deberá inferirse el tipo resultante de la multiplicación anterior. Al tipo inferido se le deberá poder aplicar el operador de dividir, con el tipo entero como multiplicando.

Deberá inferirse el tipo resultante de la división y comprobarse si éste es compatible con el tipo de superficie para llevar a cabo la asignación. Como ejemplo, si superficie fuese entera y división real, no podría llevarse a cabo la asignación.

El objetivo principal del analizador semántico de un procesador de lenguaje es asegurarse de que el programa analizado satisfaga las reglas requeridas por la especificación del lenguaje, para garantizar su correcta ejecución. El tipo y dimensión de análisis semántico requerido varía enormemente de un lenguaje a otro. En lenguajes interpretados como Lisp o Smalltalk casi no se lleva a cabo análisis semántico previo a su ejecución, mientras que en lenguajes como Ada, el analizador semántico deberá comprobar numerosas reglas que un programa fuente está obligado a satisfacer.

Vemos, pues, cómo el análisis semántico de un procesador de lenguaje no modela la semántica o comportamiento de los distintos programas construidos en el lenguaje de programación, sino que, haciendo uso de información parcial de su comportamiento, realiza todas las comprobaciones necesarias no llevadas a cabo por el analizador sintáctico para asegurarse de que el programa pertenece al lenguaje. Otra fase del compilador donde se hace uso parcial de la semántica del lenguaje es en la optimización de código, en la que analizando el significado de los programas previamente a su ejecución, se pueden llevar a cabo transformaciones en los mismos para ganar en eficiencia.

5.2 VERIFICACIÓN DE TIPOS EN EXPRESIONES

Sistema de Tipos

Reglas de un lenguaje que permiten asignar tipos a las distintas partes de un programa y verificar su corrección.

Formado por las definiciones y reglas que permiten comprobar el dominio de un identificador, y en qué contextos puede ser usado.

Cada lenguaje tiene un sistema de tipos propio, aunque puede variar de una a otra implementación.

La comprobación de tipos es parte del análisis semántico.

Funciones Principales:

Page 41: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Reglas de un lenguaje que permiten asignar tipos a las distintas partes de un programa y verificar su corrección.

Inferencia de tipos: calcular y mantener la información sobre los tipos de datos.

Verificación de tipo: asegurar que las partes de un programa tienen sentido según las reglas de tipo del lenguaje.

La información de tipos puede ser estática o dinámica:

LISP, CAML o Smalltalk utilizan información de tipos dinámica. En ADA, Pascal o C la información de tipos es estática. También puede ser una combinación de ambas formas.

Cuantas más comprobaciones puedan realizarse en la fase de compilación, menos tendrán que realizarse durante la ejecución.

Mayor eficiencia del programa objeto.

Es parte de la comprobación de tipos:

Conversión de tipos explícita: transformación del tipo de una expresión con un propósito determinado.

Coerción: conversión de tipos que realiza de forma implícita el compilador.

Conversión de tipos explícita: el programador indica el tipo destino:

Funciona como una llamada a función: recibe un tipo y devuelve otro.

Conversión de tipos implícita: el compilador convierte automáticamente elementos de un tipo en elementos de otro:

La conversión se lleva a cabo en la acción semántica de la regla donde se realiza.

Comprobador de tipos seguro: Durante la compilación (comprobación estática) detecta todos los posibles errores de tipo.

Lenguaje fuertemente tipado: Si un fragmento de código compila es que no se van a producir errores de tipo.

En la práctica, ningún lenguaje es tan fuertemente tipado que permita una completa comprobación estática.

Page 42: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Información de tipos dinámica: El compilador debe generar código que realice la inferencia y verificación de tipos durante la ejecución del programa que se está compilando.

Información de tipos estática:

Se utiliza para verificar la exactitud del programa antes de la ejecución.

Permite determinar la asignación de memoria necesaria para cada variable.

Tipo de datos = conjunto de valores + operaciones aplicables

En el ámbito de los compiladores, un tipo se define mediante una expresión de tipo (información de tipos explícita):

Nombre de tipo: float. Expresión estructurada explícita: set of integer. Estas expresiones se utilizan en la construcción de otros tipos o

para declarar variables.

También es posible incluir información de tipos implícita:

La información de tipos, implícita o explícita, se mantiene en la tabla de símbolos:

Esta información se recupera de la tabla de símbolos mediante el verificador de tipo cuando se hace referencia al nombre asociado.

Ejemplo:

Page 43: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Un lenguaje de programación contiene un conjunto de tipos predefinido denominados tipos simples:

Algunos lenguajes permiten definir nuevos tipos simples: enumerado, subrango.

Todos los lenguajes permiten crear nuevos tipos complejos a partir de otros más simples mediante constructores de tipos:

Matrices, productos, registros, punteros, funciones, … En Pascal: array, set, record, ... En C++: struct, class, union, ....

Para analizar los diferentes tipos que intervienen dentro de un programa, el compilador debe contar con una estructura interna que le permita manejar cómodamente las expresiones de tipos.

Esta estructura interna:

Debe ser fácilmente manipulable, pues su creación se realizará conforme se hace la lectura del programa fuente.

Debe permitir comparar fácilmente las expresiones asignadas a distintos trozos de código, especialmente a los identificadores de variables..

La forma más habitual de representación son los grafos acíclicos dirigidos (GADs).

La ventaja de estas representaciones es que ocupan poca memoria y por tanto la comprobación de equivalencia se efectúa con rapidez.

Ejemplos:

Page 44: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

5.3 GRAMÁTICAS DE ATRIBUTOS

Un atributo es cualquier propiedad de una construcción de un lenguaje de programación. Varían en función del tipo de información que contienen, su complejidad de cálculo y el momento en el que son calculados (en tiempo de compilación (atributos estáticos) o de ejecución (dinámicos).

Ejemplos típicos son:• El nombre de una variable.• El tipo de una variable.• El ámbito de una variable.• El valor de una expresión.• El número de argumentos de una función.• La posición en memoria de una variable.• Un fragmento de código.

Una gramática con atributos es una generalización de las gramáticas libres de contexto, denominada definición dirigida por la sintaxis:

• Cada símbolo gramatical puede tener asociado un conjunto finito de atributos, que pueden ser de los siguientes tipos:

Page 45: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Su valor se calcula en función de los atributos de los nodos hijos.

Heredados: su valor se calcula en función de los atributos de los hermanos y/o del nodo padre.

• Cada atributo tomara valores de un dominio.• Cada producción llevara asociadas un conjunto de reglas

semánticas.• Las relaciones de dependencia entre atributos, establecidas por

las reglas semánticas, se representaran mediante el Grafo de Dependencias.

Atributos Heredados

Una gramática con atributos se denomina Gramática L-Atribuida si cada atributo que se evalúa cumple una de las siguientes condiciones:

• Es un atributo sintetizado.

• Dada una producción A --> X1, X2…Xj…Xn, el atributo heredado asociado a Xj depende únicamente de los atributos de X1,…, Xj-1 y/o de atributos heredados asociados al símbolo A.

Atributos Sintetizados

En el caso de los símbolos terminales de la gramática, su atributo no es más que el lexema asociado al token reconocido por el analizador léxico.

Una gramática con atributos se denomina Gramática S-Atribuida si todos los atributos son sintetizados. Siempre es posible transformar una Gramática con Atributos en una Gramática S-Atribuida.

Ejemplos:1. Atributos SINTETIZADOS y reglas para evaluación de expresiones aritméticas.

2. Atributos HEREDADOS y reglas para propagación de tipos.

Page 46: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

5.4 MANEJO DE ERRORES SEMÁNTICOS

Errores semánticos, como operaciones realizadas sobre tipos incompatibles. Entre estos se incluyen los errores relacionados con el uso de la tabla de símbolos, como uso de identificadores no declarados o declaración doble de un identificador en la misma región de alcance.

UNIDAD VI

GENERACIÓN DE CÓDIGO INTERMEDIO

6.1 Lenguajes Intermedios

Una estructura de datos que representa el programa fuente durante la traducción se denomina REPRESENTACION INTERMEDIA o IR.

Aunque un árbol sintáctico abstracto es una representación adecuada del código del código fuente, incluso para la generación de código, no se parece ni remotamente al código objeto.

Page 47: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Una representación intermedia de esta naturaleza que se parece al código objeto se denomina CODIGO INTERMEDIO.

El código intermedio puede tomar muchas formas: existen casi tantos estilos de código intermedio como compiladores. Sin embargo, todos representan alguna forma de LINEALIZACION del árbol sintáctico, es decir, una representación del árbol sintáctico en forma secuencial. El código intermedio puede ser de muy alto nivel, representar todas las operaciones de manera casi tan abstracta como un árbol sintáctico, o parecerse mucho al código objeto.

El código intermedio es particularmente útil cuando el objetivo del compilador es producir código muy eficiente, ya que para hacerlo así se requiere una cantidad importante del análisis de las propiedades del código objeto, y esto e facilita mediante le uso del código intermedio

El código intermedio también puede ser útil al hacer que un compilador sea mas fácilmente redirigidle: si el código intermedio es hasta cierto punto independiente de la maquina objeto, entonces generar código para una maquina objetivo diferente solo requiere volver a escribir el traductor de código intermedio a código objeto y por lo regular esto es mas fácil que volver a escribir todo un generador de código.

6.2 NOTACIONES

Las notaciones son una forma especial en la que se pueden expresar una expresión matemática y puedan ser de 3 formas: infija, prefija y postfija. Los prefijos, Pre - Pos - In se refieren a la posición relativa del operador con respecto a los dos operandos.

Existen varias notaciones para representar expresiones matemáticas, que se diferencian en el orden en que se escriben los argumentos (operandos) de los operadores. Las más relevantes son:

Notación infija: La notación habitual. El orden es primer operando, operador, segundo operando.

Notación prefija: El orden es operador, primer operando, segundo operando.

Notación postfija: El orden es primer operando, segundo operando, operador.

6.2.1 NOTACION INFIJA

Es la que se utiliza en la mayoría de los casos.

Page 48: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Ejemplo:

Iré con la mas familiar de todas las notaciones, la infija si yo deseo sumar 5 mas 4; en notación infija quedaría:

5+4

En notación prefija quedaría:+ 5 4

En notación postfija:

5 4 +

Pero ahora veamos una expresión infija mas complicada como la siguiente:

5+2*(7+8)-(15-2/(20+(5/8)))

Ahora escribamos esto en forma prefija:

(+ 5 (- (* 2(+ 7 8)) (- 15 (/ 2 (+ 20 (/ 5 8))))))

La formula se ve complicada y surge la natural pregunta: Qué ventaja tiene esto? La principal ventaja es que podemos no hacer uso de las reglas de precedencia.

6.2.2 NOTACION POSTFIJA

La notación postfija es una notación muy simple en la cual se coloca un operador en el extremo derecho de una expresión, es decir, después de los operadores, en vez de estar entre ellos. Por ejemplo:

x + y – x* y

en notación postfija es

x y + x y* -

ya que x y + representa la expresión infija x + y y xy* representa x * y, etc.

Por lo regular, la notación postfija se emplea en maquinas de pilas, ya que puede manejarse con gran facilidad usando una pila. Al analizar una notación postfija de izq., a der., cada vez que se detecta un operador se mete en la pila. La ocurrencia de un operador con n operadores significa que el n – esimo operando estará en la cima de la pila. Después se sacan los operandos de la pila y se mete el resultado de la operación.

Page 49: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

6.2.3 NOTACION PREFIJA

El operador binario aparece justo antes de sus dos operandos: +AB

Una gramática que define una notación prefija puede ser:<expr_pref> := <letra> |

<operador><expr_pref><expr_pref>

<letra> := A | B | C | ... | Z

<operador> := + | - | * | /

Ejemplos en infija y su correspondiente prefija:

A+(B*C) → +A*BC(A+B)*C → *+ABC

6.3 REPRESENTACIÓN DE CÓDIGO INTERMEDIO

6.3.1 NOTACIÓN POLACA

La notación polaca es una manera de definir expresiones en las que el orden en la que aparecerán los operadores trae implícita la prioridad.

Expresiones sin paréntesis Algoritmo:

Expresiones con paréntesis Algoritmo:

1. Si es operando, PUSH(VP)2. Si es operador,

2.1 Mientras prioridad(TOP(AUX))>=prioridad(operador)POP(AUX) y PUSH(VP)2.2 PUSH(AUX) del operador

3. Si es fin de expresion,3.1 Mientras AUX no esté vacía, POP(AUX) y PUSH(VP)

Page 50: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Ejemplo:

6.3.2 CÓDIGO P

6.3 REPRESENTACIÓN DE CÓDIGO INTERMEDIO6.3.3 TRIPLES O EN 3 DIRECCIONES

La instrucción básica del código de tres direcciones esta diseñada para representar la evaluación de expresiones aritméticas y tiene la siguiente forma general:

X = Y op Z

Esta instrucción expresa la aplicación del operador op a los valores de Y y Z, y la asignación de esta valor para que sea el nuevo valor X.

1. Si es operando, PUSH(VP)2. Si es operador,

2.1 Mientras prioridad(TOP(AUX))>=prioridad(operador)POP(AUX) y PUSH(VP)2.2 PUSH(AUX) del operador

3. Si es (, PUSH(AUX) una marca4. Si es ),

4.1 Mientras TOP(AUX) < > marca POP(AUX) y PUSH(VP)4.2 POP(AUX) para quitar la marca

5. Si es fin de expresion,5.1 Mientras AUX no esté vacía, POP(AUX) y PUSH(VP)

Page 51: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Aquí op puede ser un operador aritmético como + o – o algún otro operador que pueda actuar sobre los valores de X y Y.

El nombre código en tres direcciones viene de esta forma de instrucción, ya que por lo general cada uno de los nombres X, Y y Z representan una dirección de memoria. Sin embargo, observen que el uso de la dirección de X difiere del uso de las direcciones de Y y Z, y que por tanto Y como Z pueden representar constante o valores de literales sin dirección de ejecución.

Para ver como las secuencias de código de tres direcciones de esta forma pueden representar el cálculo de una expresión, considere la expresión aritmética.

2*a+(b-3)

Con árbol sintáctico:

Sin embargo, la forma del código de tres direcciones no basta para representar todas las características. Por ejemplo, los operadores unitarios, como la negación requieren de una variación del código de tres direcciones que contenga solo dos direcciones, tal comot2 = - t1

Si se desea tener capacidad para todas las construcciones de un lenguaje de programación estándar, será necesario variar la forma del código de tres direcciones en cada construcción.Si un lenguaje contiene características poco habituales, puede ser necesario incluso inventar nuevas formas del código de tres direcciones para expresarlas.Estas son unas de las razones por las que no existe una forma estándar para el código de tres direcciones.

Page 52: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

6.3.4 CUÁDRUPLOS

Un cuádruplo es una estructura tipo registro con cuatro campos. Por ejemplo:

Por ejemplo la expresión a*b+c*d, para transformarlas a su equivalente en cuádruplos, podemos separar en unidades más pequeñas a la expresión.

Primero: a*bSegundo: c*dTercero: Hacer la suma de ambas

Ahora pasemos a hacer los cuádruplos correspondientes:

Como podemos darnos cuenta, a diferencia de los tripletes, los cuádruplos hacen uso de una variable temporal para realizar las expresiones.

Los tripletes tienen la ventaja obvia de ser más consistente, pero ellos dependen de su posición y hace que la optimización presente cambios o eliminación de código mucho más complejo.

6.4 ESQUEMAS DE GENERACIÓN

Page 53: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

6.4.1 EXPRESIONES

Toda expresión es una sentencia. El operador de asignación forma parte de una expresión.

Deberá comprobarse la validez semántica de la parte izquierda de una asignación.

Asignaciones de expresiones con tipos construidos por el usuario (registros y arrays).

Conversiones de Expresiones. Se realizarán implícitamente las conversiones de char a entero

y real y las de entero a real. S Conversiones explícitas entre tipos simples, realizadas por el

programador mediante un ahormado (cast). Conversiones explícitas de expresiones a tipos definidos por el

usuario typedef). Operadores Mínimos Los operadores obligatorios que deberá incluir el lenguaje son:

Aritméticos: +, -, *, /, resto y menos unario. Comparación: mayor, mayor o igual, menor, menor o igual,

igual y distinto. Lógicos: And, Or y Not. Sentencias de control de flujo. Sentencia condicional IF ELSE. Sentencias iterativas WHILE S Sentencia iterativa FOR Sentencias condicional múltiple CASE.

Entrada y Salida. • Sentencias de lectura y escritura múltiple por la entrada y

salida estándar para todos los tipos primitivos con las instrucciones read y write. Deberán permitirse la entrada y salida de múltiples valores (separándolos mediante comas).

Escritura de cadenas de caracteres para no tener que estar escribiendo carácter a carácter. Ejemplo: write “Escriba un número”;

Tratamiento de archivos binarios que permita: Abrir un archivo para lectura y para escritura. Leer bytes de un archivo. Escribir bytes en un archivo. Cerrar el archivo. Su tratamiento binario implica que la lectura y escritura de

valores se realizará a través de direcciones de memoria (punteros o arrays), indicando en número de bytes a leer o escribir (ver las funciones “read” y “write” del C)

6.4.2 DECLARACIÓN DE VARIABLES, CONSTANTES

Las constantes de tipos: Constantes de tipo entero. Constantes de tipo carácter. Constantes de tipo real. Constantes de tipo cadena de caracteres.

Page 54: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Declaración de variables.: • Declaración múltiple de variables, separadas por comas. • Declaración de variables globales simples (carácter, entero y

real) y construidas por el usuario (arrays y registros). • Declaración de variables locales simples y construidas por el

usuario. S Declaración de variables locales en cualquier parte del cuerpo

de la función (no necesariamente al principio de éste). S Definición de tipos construidos por el usuario (type o typedef).

La definición de estos tipos se realizará siempre con ámbito global. Su uso (declaración de variables) permitirá declarar tanto variables locales como globales, de los tipos definidos.

F Bloques y declaración de variables locales a bloques.

Ejemplos:... int i; i=3; { int i; // OK i=4; ... } // i==3

Inicialización de variables en su definición. int i=3; float r=12E-3;

Declaración de variables locales a sentencias de control.

6.4.3 ESTATUTO DE ASIGNACIÓNLa forma típica de las reglas sintácticas que regulan la asignación de un valor a una variable es la sig:

<asignación> ::=<id> := <exp> | <dereference> := <exp>

La primera regla corresponde a la asignación de valor a una variable ordinaria; la segunda a la asignación de un valor (una dirección) a un puntero. Dependiendo del lenguaje que se trate, pueden exigirse condiciones semánticas especiales a las variables afectadas por una asignación a un puntero.

Las expresiones booleanas se utilizan principalmente como parte de las proposiciones condicionales que alteran el flujo de control del programa, if-then, if-then-else, while-do. Las expresiones booleanas se componen de los operadores booleanos and, or, not aplicados a variables booleanas o expresiones relacionales.A su vez, las expresiones relacionales son de la forma E1 oprel E2, donde E1 y E2 son expresiones aritméticas y oprel es cualquier operador relacional <, >, <=, >=,....Consideremos expresiones booleanas generadas por la gramática:

Page 55: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Uno de los métodos para traducir expresiones booleanas a código de 3- direcciones consiste en codificar numéricamente los valores true y false y evaluar una expresión booleana como una expresión aritmética, siguiendo unas prioridades. A menudo se utiliza 1 para indicar true y 0 para indicar false.Las expresiones booleanas se evalúan de manera similar a una expresión aritmética de izquierda a derecha. Supongamos el ejemplo: a or b and not c. La secuencia de código de 3-direcciones correspondiente es:

t1 = a or bt2 = not ct3 = t1 and t2

6.4.4 ESTATUTO CONDICIONAL

Pasemos a considerar ahora la traducción de proposiciones de control if-then, if-then-else, while-do, generadas por la siguiente gramática:

Proposición if-then

Supongamos una sentencia if-then de la forma S→ if E then S1§ _ , ver diagrama de flujo de la figura de abajo. Para generar el código correspondiente a esta proposición habría que añadir a la función genera código () un nuevo caso para la sentencia switch que contemple este tipo de nodo en el árbol sintáctico, nodo n_if-then.

Page 56: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

TAC datos;TAC aux1, aux2;lista_codigo *cod;datos.cod=NULL;direcciones dir; // usamos dirección al salto, en vez de etiquetascase n_if-then: // childs [0]=E, childs[1]=S1

aux1=genera_codigo (childs [0]);cod=gen cuad(if_false,--, aux1.lugar, dir?);// aún no se sabe la direc. de saltoaux2=genera_codigo (childs[1]);dir=sgtedirlibre (); // relleno de retrocesorellena (cod,arg3,dir)datos.cod=concatena_codigo(aux1.cod,cod,aux2.cod);break;

Supondremos que tenemos una función, sigtedirlibre (), que guarda el índice de la siguiente instrucción libre (la función gen cuad() incrementa ese contador). En la implantación se ha optado por utilizar direcciones directamente a las instrucciones en vez de usar etiquetas.

Proposición if-then-else

Supongamos una sentencia if-then-else de la forma S → if E then S1

else S2, cuyo diagrama de flujo tiene la forma representada en

Page 57: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

la figura de abajo. Para generar el código correspondiente a esta proposición habría que añadir a la función genera_código () un nuevo caso para la sentencia switch que contemple este tipo de nodo en el árbol sintáctico, nodo n_ifthenelse. Este fragmento de código se podría implantar de forma similar a como hemos hecho en la sección anterior.

Proposición while-do

Una sentencia while - do tiene la forma S → while E do S1, cuyo diagrama de flujo viene representado en la figura de abajo. Para generar el código correspondiente a esta proposición habría que añadir a la función genera_código () un nuevo caso para la sentencia switch que contemple este tipo de nodo en el árbol sintáctico.

Supondremos que tenemos una función, sigtedirlibre (), que guarda el índice de la siguiente instrucción libre (se asume que la función gen_cuad () incrementa ese contador).

Page 58: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

TAC datos; datos.cod=NULL;TAC aux1, aux2;lista_codigo *cod1=NULL, *cod2=NULL;direcciones dir1, dir2;case n while: // childs[0]=E, childs[1]=dir1=sgtedirlibre();aux1=genera_codigo(childs[0]);cod1=gen_cuad(if_false,--, aux1.lugar, dir?);aux2=genera_codigo(childs[1]);cod2=gen_cuad(goto,--, dir1,--); // salto incondicional a dir1dir2=sigtedirlibre ();// rellena argumento de cod1, direccion salto condicionalrellena (cod1,arg3,dir2)datos.cod=concatena_codigo(aux1.cod, cod1,aux2.cod, cod2);break;

Se ha optado por utilizar direcciones directamente a las instrucciones en vez de etiquetas.

6.4.5 ESTATUTO DE CICLOS

Ciclo For

Qué decir del for. Es la sentencia más complicada puesto que hay que ejecutar cada cosa en su debido tiempo y las acciones de la gramática no se disparan en el orden que nos gustaría. Hemos usado una vez más la pila y cinco etiquetas diferentes a las que vamos saltando y colando tercetos oportunamente.

Primeramente hay que ejecutar la inicialización, generar apilar y emitir una etiqueta que será a la que se salte en la parte de abajo del for una vez se haya terminado de ejecutar el bloque. También hay que destacar que la actualización del for se tiene que producir después de ejecutar el cuerpo puesto que en él se puede alterar el valor de las variables de control.

Como funcionalidad extra, mencionar que permitimos tener múltiples inicializaciones y múltiples actualizaciones al estilo de lo que se puede hacer en C: for(a=0, b=0 ;a<5, b<3; a++, b+=1){}

Page 59: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Este Código:

for(x=1;true;x++){ x=1; break;}

[...]

generará:

0000.- 0000.- * <ASIGN , entero: 1 -> simbolo: x> /* inicializacion */ 0001.- 0001.- <ETIQUETA, e_0000> 0002.- 0002.- <BRANCH , entero: 1 -> e_0001> /* condicion */ 0003.- 0004.- <GOTO , e_0003> /* salto al cuerpo */ 0004.- 0003.- <ETIQUETA, e_0002> 0005.- 0022.- <PUSHN , 2> /* actualizacion */

Page 60: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

0006.- 0007.- <OP , SUM , simbolo: x, entero: 1 -> temporal: 0> 0007.- 0008.- <ASIGN , temporal: 0 -> simbolo: x> 0008.- 0023.- <POPN , 2> 0009.- 0012.- * <GOTO , e_0000> /* salto a condicion */ 0010.- 0011.- <ETIQUETA, e_0003> 0011.- 0013.- <ETIQUETA, e_0006> 0012.- 0000.- * <ASIGN , entero: 1 -> simbolo: x> /* cuerpo del for */ 0013.- 0015.- <GOTO , e_0004> /* break */ 0014.- 0016.- <ETIQUETA, e_0005> 0015.- 0019.- <GOTO , e_0002> /* salto a actualizacion */ 0016.- 0012.- * <GOTO , e_0000> 0017.- 0017.- <ETIQUETA, e_0007> 0018.- 0018.- <ETIQUETA, e_0004> 0019.- 0020.- <ETIQUETA, e_0001>

6.4.6 ARREGLOS

Una referencia de arreglos involucra la subindización de una variable de arreglo mediante una expresión para obtener una referencia o valor de un simple elemento, como en el código en c.

int a [SIZE]; int i,j;…a[i+1] = a [j*2]+3;

En esta asignación la subindización de a mediante la expresión i +1 produce una dirección, mientras que la subindización de a mediante la expresión j * 2 produce el valor de la dirección calculada del tipo de elemento de a ( a saber , int). Los arreglos son almacenados de manera secuencial en la memoria, cada dirección debe ser calculada desde la dirección base de a y un desplazamiento que depende linealmente del valor del subíndice. Cuando se deseara obtener el valor mas que la dirección, se debe generar un paso de indirección extra para obtener el valor de la dirección calculada.

De manera general, la dirección de un elemento de arreglo a [t] en cualquier lenguaje es:

dirección _ base(a) + (t- límite _ inferior (a))* tamaño _ elemento(a)

Por ejemplo, la dirección de la referencia de un arreglo en C a[i+1] es:

a + ( i + 1 ) * sizeof (int)

6.4.7 ESTATUTO FUNCIONES

Page 61: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

El diseño del manejo de funciones por un compilador implica dar respuesta a las siguientes preguntas:

¿Cómo se comunican los argumentos desde el programa que invoca a la función invocada?

¿Cómo se comunican los resultados desde la función invocada al programa que invoca?

En general, se utiliza una pila para almacenar las variables locales de la función (variables automáticas) y los argumentos de llamada a la función. En algunos lenguajes, como C y C++, los argumentos se guardan en la pila de orden inverso (de derecha a izquierda). En otros lenguajes se hace al revés. Para pasar el resultado de la función al programa que la invocó, se suele utilizar el registro EAX, siempre que dicho resultado quepa en él. De lo contrario se puede utilizar la pila, o bien la memoria estática.

UNIDAD VII

OPTIMIZACIÓN

7.1 TIPOS DE OPTIMIZACIÓN

Objetivo: Mejorar código objeto final, preservando significado del programa.

Se sigue una aproximación conservadora No se aplican todas las posibles optimizaciones, solo las “seguras”CLASIFICACIÓN DE LAS OPTIMIZACIONES:

1. En función de la dependencia de la arquitectura:Dependientes de la máquina: Aprovechan características específicas de la máquina.Objetivo:• Asignación de registros, uso de modos de direccionamiento• Uso instrucciones especiales (IDIOMS)• Relleno de pipelines, predicción de saltos, aprovechamiento de estrategias de memoria caché, etc.

Independientes de la máquina: Aplicables en cualquier tipo de máquina.Objetivo:• Ejecución en tiempo de compilación• Eliminación de redundancias• Cambios de orden de ejecución, etc.

Factores a Optimizar:

Velocidad de ejecuciónTamaño del programaNecesidades de memoria

Page 62: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

2. En función del ámbito de aplicación

Optimizaciones locales: Aplicadas dentro de un Bloque Básico.• Sólo estudian las instrucciones del B.B. actual

Optimizaciones globales: Aplicadas a más de un B.B.• Consideran contenido y flujo de datos entre todos o parte de los B.B.• Necesidad de recoger info. sobre los B.B. y sus interrelaciones.

7.1.1 LOCALES

1. Ejecución en tiempo de compilación.• Precalcular expresiones constantes (con constantes o variables

cuyo valor no cambia).

2. Reutilización de expresiones comunes.

3. Propagación de copias.• Ante instrucciones f = a, sustituir todos los usos de f por a

4. Eliminación redundancias en acceso matrices.• Localizar expresiones comunes en cálculo direcciones de

matrices

Cod. fuente:A: array [1..4, 1..6, 1..8] of integerA[i,j,5] := A[i,j,6] + A[i,j,4]

Sin optimización:direc(A[i,j,5]) = direc(A[1,1,1]) + (i-1)*6*8 + (j-1)*8 + (5-1)direc(A[i,j,6]) = direc(A[1,1,1]) + (i-1)*6*8 + (j-1)*8 + (6-1)

i = 2 + 3 i = 5j = 4 → j = 4f = j + 2.5 f = 6.5

a = b + c a = b + cd = a – d d = a - de = b + c e = af = a – d f = a - d

a = 3 + if = a a = 3 + ib = f + c b = a + cd = a + m d = a + mm = f + d m = a + d

Page 63: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

direc(A[i,j,4]) = direc(A[1,1,1]) + (i-1)*6*8 + (j-1)*8 + (4-1)

Con optimización:k := direc(A[1,1,1]) + (i-1)*6*8 + (j-1)*8 + (5-1)direc(A[i,j,5]) = k + 4direc(A[i,j,6]) = k + 5direc(A[i,j,4]) = k + 3

5. Transformaciones algebraicas:Aplicar propiedades matemáticas para simplificar expresiones

a) Eliminación secuencias nulas

b) Reducción de potencia.• Reemplazar una operación por otra equivalente menos costosa.

c) Reacondicionamiento de operadores.• Cambiar orden de evaluación aplicando propiedades

conmutativa, asociativa y distributiva.

7.1.2 BUCLES

Los bucles son una parte del código muy propensa a la ineficiencia. Su naturaleza multiplica la diferencia de rendimiento entre dos versiones de código equivalentes, que fuera de un bucle presentarían una mejora significativa. Por esta razón es importante generar el código de manera cuidadosa, para introducir en los cuerpos de los bucles sólo lo que sea estrictamente necesario, en versiones que

x + 0 → x1 * x → xx / 1 → x

x2 → x * x

2*x → x + x (suma); x<<1 (despl. izq.)

4*x, 8*x,... → x<<2, x<<3,...

x / 2 → x>>2

A := B*C*(D + E) ≡ A := (D + E)*B*CMOV B,R0MUL C,R0 MOV D,R0MOV Ro, t1 ADD E,R0 5 instruccionesMOV D,R0 MUL B,R0 0 temporalesADD E, R0 MUL C,R0 MUL t1,R0 MOV R0,AMOV R0,A

Page 64: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

minimicen el costo para la maquina. El primer enfoque indica los invariantes del bucle; el segundo se llama reducción de fuerza. Una operación es invariante respecto a un bucle si ninguno de los operándos de los que depende cambia de valor durante la ejecución del bucle. La optimización consiste en sacar la operación fuera del bucle. Para estudiar la reducción de fuerza, será conveniente considerar un ejemplo previo.

Considere el siguiente bucle, escrito en lenguaje C:

For (i=a; i<c; i+=b) {…d=i*k;…}

Se supone que: b y k no se modifican dentro del cuerpo del bucle. i, variable del bucle, solo se modifica para incrementarla. La única modificación del valor de la variable d dentro del

cuerpo del bucle es la que se muestra.

Se puede comprobar que los valores que toma la variable d son los siguientes:

a * k(a + b) * k

(a + 2 * b) * k

Es posible obtener una versión equivalente del bucle, que realice menos trabajo en cada iteración, reduciendo el calculo del valor siguiente de la variable d a un incremento, que se le sumará como si se tratase de una variable del bucle (como la variable i):

d = a * k;t1 = b * k;for (i=a; i<c; i+=b, d+=t1) {…}

Obsérvese: La aparición de una nueva variable (t1), que se hace cargo de

parte del calculo del valor de d. La conversión de d en una variable del bucle, que en lugar de

calcularse por completo en el cuerpo del bucle simplemente se incrementa.

Que los valores que recibe la variable d en cada ejecución del bucle coinciden con los indicados anteriormente.

La optimización consiste en que el cálculo que se repite dentro del bucles es una suma, en lugar de un producto, y los productos suelen ser mas costosos para el sistema.

7.1.3 GLOBALES

Optimizaciones entre Bloques Básicos

Optimizaciones típicas:

Page 65: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

• Identificación de expresiones comunes entre bloques

• Optimización de llamadas a procedimientos• Optimización de bucles

Optimización Llamadas a Procedimientos:

Llamadas a procedimientos son muy costosas• Cambios del ámbito de referencias• Gestión de Registros de Activación• Paso de parámetros + asignación de datos locales

Mejoras1. Optimizar manejo de Registro de Activación

• Minimizar copia de parámetros y nº. registros a salvar• Uso almacenamiento estático si no hay llamadas

recursivas2. Expansión en línea.Idea: Eliminar llamadas a procedimientos “copiando” el cuerpo del procedimiento en el lugar donde es llamado.

• Evita sobrecarga asociada con llamadas a procedimientos.

• Permite optimizaciones adicionales.

Limitaciones:• Aumenta uso de memoria → útil en procedimientos pequeños y

llamados desde pocos lugares.

• No aplicable en procedimientos recursivos•

Ejemplos:

• Directiva inline en C++ y Java• En C puede simularse con macros #define

7.1.4 DE MIRILLA

Aplicable en código intermedio o código objeto.

Constituye una nueva fase aislada.

Idea Básica:• Se recorre el código buscando combinaciones de instrucciones

que puedan ser reemplazadas por otras equivalentes más eficientes.

→ El código llamado pasa a ser parte del código que lo llama

→ Si se llama en un único lugar (es frecuente), no supone costo de espacio adicional

Page 66: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

• Se utiliza una ventana de n instrucciones y un conjunto de patrones de transformación (patrón, secuencias reemplazan).

• Si las instrucciones de la ventana encajan con algún patrón se reemplazan por la secuencia de emplazamiento asociada.

• Las nuevas instrucciones son reconsideradas para las futuras optimizaciones

Ejemplos:• Eliminación de cargas innecesarias

• Reducción de potencia• Eliminación de cadenas de saltos

7.2 COSTOS7.2.1 COSTOS DE EJECUCION

Un generador de código no solo debería buscar opciones innecesarias, sino que debería tomar ventaja de las oportunidades para reducir el costo de las operaciones que son necesarias, pero que se pueden implementar de maneras mas económicas que lo que puede indicar el código fuente o una implementación simple. Un ejemplo típico de esto es el reemplazo de las operaciones aritméticas por operaciones mas económicas. Por ejemplo, la multiplicación por 2 puede implementarse como una operación de desplazamiento, y una potencia entera pequeña, tal como x3, puede implementarse como una multiplicación, tal como x*x*x. Esta optimización se conoce como reducción de potencia.

Una operación que en ocasiones puede ser relativamente costosa es la llamada de procedimientos, donde se deben realizar muchas operaciones de secuencia de llamada. Los procesadores modernos han reducido este costo de manera sustancia, ofreciendo apoyo a hardware para secuencias de llamada estándar, pero la eliminación de llamadas frecuentes a procedimientos pequeños aún puede producir aumentos de velocidad mensurables.

Page 67: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Durante la generación de código final se pueden encontrar algunas últimas oportunidades para reducir el costo de ciertas operaciones utilizando instrucciones especiales disponibles en la maquina objetivo.

7.2.2 CRITERIOS PARA MEJORAR EL CODIGO

Existen tantas y diversas técnicas de optimización de código que solo podemos dar una pequeña visión fugaz de las mas importantes y las mas ampliamente utilizadas.

Al juzgar si la implementación de una técnica de optimización en particular es demasiada compleja respecto a sus resultados en la mejora real del código, es importante determinar no solo la complejidad de la implementación en términos de las estructuras de datos y código de compilador extra, sino también es el efecto que el paso de optimización pueda tener sobre la velocidad de ejecución del compilador mismo.

Dentro de los criterios para mejorar nuestro código tenemos:

Asignación de Registro.- El buen uso de los registros es la característica más importante de código eficiente.

Operaciones Innecesarias.- La segunda fuente principal de mejoramiento de código es el evitar la generación de código para las operaciones que sean redundantes o innecesarias.

Operaciones Costosas.- Un generado de código no sólo debería buscar operaciones innecesarias, sino que debería tomar ventaja de las oportunidades para reducir el costo de operaciones que son necesarias, pero que se pueden implementar de maneras más económicas que lo que puede indicar el código fuente o una implementación simple.

UNIDAD VIII

GENERACION DE CODIGO OBJETO

8.1 LENGUAJE MAQUINA

Es un conjunto de instrucciones restringido y sencillo, que puede ser interpretado y ejecutado directamente por el computador.

¿Que estudia el Nivel del Lenguaje Maquina?

Page 68: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Registros Programables que hay en la CPU• Numero de Registros• Tamaño• Utilidad de cada uno

Memoria Principal• Cantidad de memoria principal que se puede direccionar (nº de

palabras)• Longitud de palabra• Unidad mínima redireccionable

Instrucciones maquinas• Tipos de instrucciones maquinas• Repertorio o conjunto de instrucciones• Formato de las instrucciones• --Código de operación• --Direccionamiento• Modos de direccionamiento

8.1.1 CARACTERÍSTICAS

Las instrucciones se almacenan y tratan en el computador como cadenas de unos y ceros, aunque se representen en hexadecimal o mediante nemotécnicos.

Las instrucciones maquinan suelen, en general, cumplir las siguientes propiedades:

1.- Realizan una única y sencilla función:

2.- Emplean un numero fijo de operándoos, que podrán ser implícitos o estar representados explícitamente en la instrucción.

3.- La codificación de las instrucciones es bastante sistemática.

Para que su codificación sea sencilla

4.- Las instrucciones son autocontenidas e independientes.

Contiene toda la información necesaria paraAutocontenidas

ejecutarse

Su interpretación es sencilla

Page 69: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

No requieren información de otras instrucciones

IndependientesSu interpretación es independiente de la

posiciónque ocupan en el programa o en la memoria.

8.1.2 MODOS DE DIRECCIONAMIENTO

La denominación de modos de direccionamiento proviene que normalmente se especifica la dirección donde se encuentra el dato o la instrucción.

Cuando se utiliza una posición de memoria, la dirección real de memoria especificada por el modo de direccionamiento se denomina DIRECCION EFECTIVA (EA, Effective Addres).

El modo de direccionamiento esta codificado en algún/algunos campos de la instrucción.

TIPOS DE DIRECCIONAMIENTO

Son las diversas formas de determinar el valor de un operando o la posición de un operando o una instrucción.

OBJETO Instrucción, operando o resultado que se desea direccionar.

La propia instrucciónPuede residir en Un registro

En la memoria principal

Direccionamiento Implícito

Direccionamiento InmediatoA registro (Direccionamiento Registro)

Direccionamiento DirectoA memoria (Direccionamiento Directo o

Absoluto

A través de registroDireccionamiento Indirecto

A través memoria

A Registro Base Preautoincremento Preautodecremento

A Registro Índice Postautoincremento Postautodecremento

Direccionamiento RelativoIndexado Respecto a Base

Al contador del Programa

A Pila

Page 70: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

8.2 LENGUAJE ENSAMBLADOR

La comunicación en lenguaje de máquina es particular de cada procesador que se usa, y programar en este lenguaje es muy difícil y tedioso, por lo que se empezó a buscar mejores medios de comunicación con ésta.

A principios de la década de 1950, y con el fin de facilitar la labor de los programadores, se desarrollaron códigos mnemotécnicos para las operaciones y direcciones simbólicas. Uno de los primeros pasos para mejorar el proceso de preparación de programas fue sustituir los códigos de operación numéricos del lenguaje de máquina por símbolos alfabéticos, que conforman un lenguaje mnemotécnico. Todas las computadoras actuales tienen códigos mnemotécnicos aunque, naturalmente, los símbolos que se usan varían en las diferentes marcas y modelos. La computadora sigue utilizando el lenguaje de máquina para procesar los datos, pero los programas ensambladores traducen antes los símbolos de código de operación especificados a sus equivalentes en lenguaje de máquina. Los lenguajes ensambladores tienen ventajas sobre los lenguajes de máquina. Ahorran tiempo y requieren menos atención a detalles. Se incurren en menos errores y los que se cometen son más fáciles de localizar. Además, los programas en lenguaje ensamblador son más fáciles de modificar que los programas en lenguaje de máquina. Pero existen limitaciones. La codificación en lenguaje ensamblador es todavía un proceso lento. Además, una desventaja importante de estos lenguajes es que tienen una orientación a la máquina. Es decir, están diseñados para la marca y modelo específico de procesador que se utiliza.

En el principio de la computación este era el lenguaje que tenía que "hablar" el ser humano con la computadora y consistía en

Page 71: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

insertar en un tablero miles de conexiones y alambres y encender y apagar interruptores. Aunque en la actualidad ya no se emplea, es importante reconocer que ya no es necesario que nos comuniquemos en este lenguaje de "unos" y "ceros", pero es el que internamente una computadora reconoce o "habla".

8.2.1 CARACTERISTICAS

Dentro de las características más primordiales que encontramos en el Lenguaje Ensamblador tenemos las siguientes las cuales se pueden tomar como ventajas respecto a los lenguajes de alto nivel:

Velocidad Eficiencia de tamaño Flexibilidad

Velocidad:El proceso de traducción que realizan los intérpretes, implica un

proceso de cómputo adicional al que el programador quiere realizar. Por ello, nos encontraremos con que un intérprete es siempre más lento que realizar la misma acción en Lenguaje Ensamblador, simplemente porque tiene el costo adicional de estar traduciendo el programa, cada vez que lo ejecutamos. De ahí nacieron los compiladores, que son mucho más rápidos que los intérpretes, pues hacen la traducción una vez y dejan el código objeto, que ya es Lenguaje de Máquina, y se puede ejecutar muy rápidamente. Aunque el proceso de traducción es más complejo y costoso que el de ensamblar un programa, normalmente podemos despreciarlo, contra las ventajas de codificar el programa más rápidamente. Sin embargo, la mayor parte de las veces, el código generado por un compilador es menos eficiente que el código equivalente que un programador escribiría. La razón es que el compilador no tiene tanta inteligencia, y requiere ser capaz de crear código genérico, que sirva tanto para un programa como para otro; en cambio, un programador humano puede aprovechar las características específicas del problema, reduciendo la generalidad pero al mismo tiempo, no desperdicia ninguna instrucción, no hace ningún proceso que no sea necesario. Para darnos una idea, en una PC, y suponiendo que todos son buenos programadores, un programa para ordenar una lista tardará cerca de 20 veces más en Visual Basic (un intérprete), y 2 veces más en C (un compilador), que el equivalente en Ensamblador. Por ello, cuando es crítica la velocidad del programa, Ensamblador se vuelve un candidato lógico como lenguaje.

Ahora bien, esto no es un absoluto; un programa bien hecho en C puede ser muchas veces más rápido que un programa mal hecho en Ensamblador; sigue siendo sumamente importante la elección

Page 72: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

apropiada de algoritmos y estructuras de datos. Por ello, se recomienda buscar optimizar primero estos aspectos, en el lenguaje que se desee, y solamente usar Ensamblador cuando se requiere más optimización y no se puede lograr por estos medios.

Eficiencia de Tamaño:Por las mismas razones que vimos en el aspecto de velocidad,

los compiladores e intérpretes generan más código máquina del necesario; por ello, el programa ejecutable crece. Así, cuando es importante reducir el tamaño del ejecutable, mejorando el uso de la memoria y teniendo también beneficios en velocidad, puede convenir usar el lenguaje Ensamblador. Entre los programas que es crítico el uso mínimo de memoria, tenemos a los virus y manejadores de dispositivos (drivers). Muchos de ellos, por supuesto, están escritos en lenguaje Ensamblador.

Flexibilidad:Las razones anteriores son cuestión de grado: podemos hacer

las cosas en otro lenguaje, pero queremos hacerlas más eficientemente. Pero todos los lenguajes de alto nivel tienen limitantes en el control; al hacer abstracciones, limitan su propia capacidad. Es decir, existen tareas que la máquina puede hacer, pero que un lenguaje de alto nivel no permite. Por ejemplo, en Visual Basic no es posible cambiar la resolución del monitor a medio programa; es una limitante, impuesta por la abstracción del GUI Windows. En cambio, en ensamblador es sumamente sencillo, pues tenemos el acceso directo al hardware del monitor. Resumiendo, la flexibilidad consiste en reconocer el hecho de que

Todo lo que puede hacerse con una máquina, puede hacerse en el lenguaje ensamblador de esta máquina; los lenguajes de alto nivel tienen en una u otra forma limitante para explotar al máximo los recursos de la máquina.

Por otro lado, al ser un lenguaje más primitivo, el Ensamblador tiene ciertas desventajas respecto a los lenguajes de alto nivel:

Tiempo de programación Programas fuente grandes Peligro de afectar recursos inesperadamente Falta de portabilidad

Tiempo de Programación:Al ser de bajo nivel, el Lenguaje Ensamblador requiere más

instrucciones para realizar el mismo proceso, en comparación con un lenguaje de alto nivel. Por otro lado, requiere de más cuidado por parte del programador, pues es propenso a que los errores de lógica se reflejen más fuertemente en la ejecución.

Page 73: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

Por todo esto, es más lento el desarrollo de programas comparables en Lenguaje Ensamblador que en un lenguaje de alto nivel, pues el programador goza de una menor abstracción.

Programas Fuentes Grandes:Por las mismas razones que aumenta el tiempo, crecen los

programas fuentes; simplemente, requerimos más instrucciones primitivas para describir procesos equivalentes. Esto es una desventaja porque dificulta el mantenimiento de los programas, y nuevamente reduce la productividad de los programadores.

Peligro de afectar recursos Inesperadamente:Tenemos la ventaja de que todo lo que se puede hacer en la

máquina, se puede hacer con el Lenguaje Ensamblador (flexibilidad). El problema es que todo error que podamos cometer, o todo riesgo que podamos tener, podemos tenerlo también en este Lenguaje. Dicho de otra forma, tener mucho poder es útil pero también es peligroso. En la vida práctica, afortunadamente no ocurre mucho; sin embargo, al programar en este lenguaje verán que es mucho más común que la máquina se "cuelgue", "bloquee" o "se le vaya el avión"; y que se reinicialize. ¿Por qué?, porque con este lenguaje es perfectamente posible (y sencillo) realizar secuencias de instrucciones inválidas, que normalmente no aparecen al usar un lenguaje de alto nivel. En ciertos casos extremos, puede llegarse a sobrescribir información del CMOS de la máquina (no he visto efectos más riesgosos); pero, si no la conservamos, esto puede causar que dejemos de "ver" el disco duro, junto con toda su información.

Falta de Portabilidad:Como ya se mencionó, existe un lenguaje ensamblador para

cada máquina; por ello, evidentemente no es una selección apropiada de lenguaje cuando deseamos codificar en una máquina y luego llevar los programas a otros sistemas operativos o modelos de computadoras. Si bien esto es un problema general a todos los lenguajes, es mucho más notorio en ensamblador: yo puedo reutilizar un 90% o más del código que desarrollo en "C", en una PC, al llevarlo a una RS/6000 con UNIX, y lo mismo si después lo llevo a una Macintosh, siempre y cuando esté bien hecho y siga los estándares de "C", y los principios de la programación estructurada. En cambio, si escribimos el programa en Ensamblador de la PC, por bien que lo desarrollemos y muchos estándares que sigamos, tendremos prácticamente que reescribir el 100 % del código al llevarlo a UNIX, y otra vez lo mismo al llevarlo a Mac.

Page 74: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

8.2.2 ALMACENAMIENTO

Dentro del almacenamiento que maneja el lenguaje ensamblador se tiene que este tiene una relación con los componentes internos del procesador:

En la memoria: En donde se almacena la información en celdas especiales llamados registros los cuales tienen un nivel alto y un nivel bajo.

Unidad aritmética y lógica: Es la responsable de realizar como su nombre lo indica operaciones aritméticas y lógicas.

Unidad de control: Se encarga de coordinar que los otros componentes ejecuten las operaciones correctamente.

Bus interno: Son los canales por donde pasa la información que la máquina va a procesar (bus de entrada) o procesada (bus de salida).

Manejo de la Memoria: Direccionamiento (Interno y Externo)

El manejo de la memoria depende de que procesador tenga la máquina, entre los cuales a continuación se mencionan los siguientes:

Memoria de Programa Memoria Externa de Datos Memoria Interna de Datos Registros de Funciones Especiales Memoria de Bit

El espacio de la Memoria de Programa contiene todas las instrucciones, datos, tablas y cadenas de caracteres (strings) usadas en los programas. Esta memoria se direcciona principalmente usando el registro de 16 bits llamado Data Pointer. El tamaño máximo de la Memoria de Programa es de 64 Kbytes.

La Memoria Externa de Datos contiene todas las variables y estructuras de datos que no caben en la memoria interna del Microprocesador. Esta memoria se direcciona principalmente por el registro de 16 bits Data Pointer , aunque también se puede direccionar un banco de Memoria Externa de Datos de 256 bytes usando los dos primeros registros de propósito general.

El espacio de Memoria Interna de Datos funcionalmente es la memoria de datos más importante, ya que ahí es donde residen cuatro bancos de registros

de propósito general; la pila o stack del programa; 128 bits de los 256 bits de un área de memoria direccionable por bit y todas las variables y estructuras de

Page 75: Manual Completo Para Descargar

PROGRAMACIÓN DE SISTEMAS

datos operadas directamente por el programa. El tamaño máximo de la Memoria Interna de Datos es de 256 bytes.

Contiene un espacio para los denominados Registros de Funciones Especiales destinado para los puertos de entrada/salida, temporizadores y puerto serie del circuito integrado. Estos registros incluyen al Stack Pointer; al registro de la palabra de estado del programa y al Acumulador. La cantidad máxima de Registros de Funciones Especiales es 128. Todos los Registros de Funciones Especiales tienen direcciones mayores a 127 y se ubican en los 128 bytes superiores de la Memoria Interna de Datos. Estas dos áreas de la Memoria Interna de Datos se diferencian por el modo de direccionamiento usado para accesarlas. Los Registros de Funciones Especiales solo se pueden accesar usando el modo de direccionamiento Directo, mientras que los 128 bytes superiores solo se pueden accesar con el modo de direccionamiento Indirecto.

Por otra parte, el espacio de Memoria de Bit se usa para almacenar variables y banderas de un bit. El tamaño máximo de la Memoria de Bit es de 256 bits, 128 de los bits comparten su espacio con 16 bytes del espacio de la Memoria Interna de Datos y los otros 128 bits lo hacen con los Registros de Funciones Especiales.

8.3 OTROS LENGUAJES PARA VIRTUAL MACH (JVM)

Los lenguajes basados en una máquina virtual, comúnmente son más rápidos que los totalmente interpretados, debido a que utilizan una arquitectura de código intermedio. La idea es dividir la tarea de ejecutar un programa en dos partes. En la primera, se realiza el análisis léxico y sintáctico del programa fuente, para generar el programa en instrucciones del procesador virtual (código intermedio) y en el segundo paso, se itera sobre el código intermedio para obtener la ejecución final del programa. Los lenguajes compilados de código intermedio, pueden llegar a ser un orden de magnitud más rápido que los lenguajes completamente interpretados, pero, por consiguiente, un orden de magnitud más lentos que lenguajes optimizados como C o C++.