Antología Lenguajes Y Autómatas

Anuncio
INSTITUTO TECNOLOGICO SUPERIOR DE ZONGOLICA
LENGUAJES Y AUTOMATAS II
LSC. RAFAEL JUAN CORDOBA DE LA LLAVE
MAYO 2014
INTRODUCCION
El desarrollo de software se puede dividir en 2 grandes categorías. El
software comercial y el software científico. La presente materia proporciona las
bases para crear software científico. Es común pensar que el estudiante normal
nunca tendrá acceso a desarrollar software científico, sin embargo, esto es un
error ya que cada día las necesidades se van ampliando y todo va siendo más
accesible. Tenemos el clásico ejemplo de la robótica. Actualmente ya hay tiendas
donde venden accesorios para hacer un pequeño robot de juguete. Eso antes no
se veía y ahora ya es común.
Por lo mismo, es importante proporcionarle al alumno las bases para que él se
introduzca con profundidad en el mundo de los compiladores. Esta materia abre
horizontes impresionantes ya que se conoce a fondo las etapas por las que
atraviesa la creación de un lenguaje de computación. Desde la etapa léxica hasta
la etapa de generación de código, el estudiante debe profundizar en
conocimientos que colindan con la parte electrónica de la computadora, el
lenguaje ensamblador, el lenguaje máquina.
Esta materia es una aventura racional. Algunos pensarán que es un tormento
cerebral, pero los inteligentes sabrán apreciar todas las competencias que se
desarrollan en esta materia.
Cabe mencionar que esta materia es la 2ª. Parte de la materia Lenguajes y
autómatas, por lo tanto se debe dedicar cierto tiempo a dar un repaso práctico a la
1ª. Parte de la materia que consistió en las 2 primeras fases de los compiladores:
fase léxica y fase sintáctica.
Si no se da este repaso se corre el peligro de que el alumno no entienda ésta
segunda parte ya que van muy ligadas.
Es muy recomendable utilizar un compilador didáctico.
Se recomienda
ampliamente el compilador desarrollado por Kenneth Louden. En la bibliografía al
final de este documento se encuentra con la referencia número 3 y número 12.
Esto debido a que el alumno debe conocer un compilador ya hecho para así
entender al 100% todos los conceptos.
PROPOSITO
Y
CONTENIDO
En esta asignatura se debe desarrollar el análisis semántico, la generación de
código, la optimización y la generación del código objeto para obtener el
funcionamiento de un compilador.
Esta asignatura busca proveer al estudiante de herramientas, conocimientos y
habilidades necesarias para desarrollar un compilador con base en los conocimientos
previos de la asignatura lenguajes y autómatas I. La aportación de esta materia es
relevante en el ámbito del desarrollo de software de sistemas.
Es indispensable distinguir que la carrera de Ingeniería en Sistemas Computacionales
se basa no sólo en el desarrollo de software comercial y administrativo, sino también
en el desarrollo de software científico. Esta materia se ubica en la segunda categoría
y es indispensable desarrollar software en estos campos para preparar a los
egresados y tengan la posibilidad de cursar posgrados de alto nivel.
La asignatura trata de concretar un traductor iniciado en la materia previa para que el
estudiante comprenda que es capaz, mediante técnicas bien definidas, de crear su
propio lenguaje de programación.
La aportación de la asignatura al perfil del egresado será específicamente la siguiente:
• Desarrollar, implementar y administrar software de sistemas o de aplicación que
cumpla con los estándares de calidad buscando como finalidad apoyar la
productividad y competitividad de las organizaciones.
• Integrar soluciones computacionales con diferentes tecnologías, plataformas o
dispositivos.
• Diseñar e implementar interfaces hombre – máquina y maquina – máquina para la
automatización de sistemas.
• Identificar y comprender las tecnologías de hardware para proponer, desarrollar y
mantener aplicaciones eficientes.
OBJETIVO
Desarrollar software de base: traductor, intérprete o compilador.
COMPETENCIAS PREVIAS Y RELACION CON CURSOS ANTERIORES Y
POSTERIORES
Definir, diseñar, construir y programar las fases del analizador léxico y sintáctico de un
traductor o compilador.
Su relación con materias anteriores: Fundamentos de programación, Tópicos
avanzados de programación, Fundamentos de Ingeniería de Software, Lenguajes
y autómatas I.
Su relación con materias posteriores: Sistemas programables
Las competencias logradas en esta materia son: razonamiento deductivo e
inductivo, análisis – síntesis.
CONTENIDO
UNIDAD 1 Análisis semántico
1.1.
Arboles de expresiones.
1.2.
Acciones semánticas de un analizador sintáctico.
1.3. Comprobaciones de tipos en expresiones .
1.4. Pila semántica en un analizador sintáctico.
1.5. Esquema de traducción.
1.6. Generación de la tabla de símbolo y de direcciones.
1.7. Manejo de errores semánticos.
UNIDAD 2 Generación de código intermedio.
2.1 Notaciones
2.1.1 Prefija
2.1.2 Infija
2.1.3 Postfija
2.2 Representaciones de código Intermedio.
2.2.1 Notación Polaca
2.2.2 Código P
2.2.3 Triplos
2.2.4 Cuádruplos.
2.3 Esquema de generación.
2.3.1 Variables y constantes.
2.3.2 Expresiones.
2.3.3 Instrucción de asignación.
2.3.4 Instrucciones de control.
2.3.5 Funciones
2.3.6 Estructuras
UNIDAD 3 Optimización
3.1 Tipos de optimización.
3.1.1 Locales.
3.1.2 Ciclos.
3.1.3 Globales.
3.1.4 De mirilla.
3.2 Costos.
3.2.1 Costo de ejecución. (memoria, registros, pilas)
3.2.2 Criterios para mejorar el código.
3.2.3 Herramientas para el análisis del flujo de datos.
UNIDAD 4 Generación de código objeto
4.1 Registros.
4.2 Lenguaje ensamblador.
4.3 Lenguaje maquina.
4.4 Administración de memoria.
COMPETENCIAS A ALCANZAR EN EL CURSO
Al término del curso el participante logrará las siguientes competencias:
Unidad 1: Análisis Semántico.
Diseñar mediante el uso de arboles de expresiones dirigida por la sintaxis un
analizador semántico para un meta-compilador.
Unidad 2: Generación de código intermedio.
Aplicar las herramientas para desarrollar una máquina virtual que ejecute código
intermedio a partir del código fuente de un lenguaje prototipo.
Unidad 3: Optimización.
Conocer e Identificar los diferentes tipos de optimización que permita eficientar el
código intermedio.
Unidad 4: Generación del código objeto.
Utilizar un lenguaje de bajo nivel para traducir el código construido a lenguaje
máquina para su ejecución.
UNIDAD 1 Análisis Semántico.
Competencia específica de la unidad:
Diseñar mediante el uso de arboles de expresiones dirigida por la sintaxis un
analizador semántico para un meta-compilador.
CONTENIDO TEMATICO
En este capítulo analizamos la fase del compilador que calcula la información
ad~cional necesaria para la compilación una vez que se conoce la estructura
sintáctica de un programa. Esta fase se conoce como análisis semántico debido a que
involucra el cálculo de información que rebasa las capacidades de las gramáticas
libres de contexto y los algoritmos de análisis sintáctico estándar, por lo que no se
considera como sintaxis.'
La información calculada tambikn está estrechamente relacionada con el significado
final, o semántica, del programa que se traduce. Como el análisis que realiza un
compilador es estático por definición (tiene lugar antes de la ejecución), dicho análisis
semántico también se conoce como análisis semántico estático. En un lenguaje
típico estáticamente tipificado como C. el análisis semántico involucra la
construcción de una tabla de símbolos para mantenerse al tanto de los significados de
nombres establecidos en declaraciones e inferir tipos y verificarlos en expresiones y
sentencias con el fin de determinar su exactitud dentro de las reglas de tipos del
lenguaje.
El análisis semántico se puede dividir en dos categorías. La primera es el análisis de
un programa que requiere las reglas del lenguaje de programación para establecer su
exactitud y garantizar una ejecución adecuada. La complejidad de un análisis de esta
clase requerido por una definición del lenguaje varía enormemente de lenguaje a
lenguaje. En lenguajes orientados en forma dinámica, tales como LISP y Smalltalk,
puede no haber análisis semántico estático en absoluto, mientras que en un lenguaje
como Ada existen fuertes requerimientos que debe cumplir un programa para ser
ejecutable. Otros lenguajes se encuentran entre estos extremos (Pascal, por ejemplo.
no es tan estricto en sus requerimientos estáticos como Ada y C, pero no es tan
condescendiente como LISP).
La segunda categoría de análisis semántico es el análisis realizado por un compilador
para mejorar la eficiencia de ejecución del programa traducido. Esta clase de análisis
por lo regular se incluye en análisis de "optimización", o técnicas de mejoramiento de
código. Investigaremos algunos de estos métodos en el capitulo sobre generación de
còdigo, mientras que en este capitulo nos enfocaremos en los análisis comunes que
por exactitud son requeridos para una definición del lenguaje. Conviene advertir que
las técnicas estudiadas aqui se aplican a ambas situaciones. También que las dos
categorías no son mutuamente excluyentes, ya que los requerimientos de exactitud,
tales como la verificación de tipos estáticos, también permiten que un compilador
genere código más eficiente que para un lenguaje sin estos requerimientos. Además,
vale la pena hacer notar que los requerimientos de exactitud que aqui se comentan
nunca pueden establecer Ia exactitud completa de un programa, sino sólo una clase
de exactitud parcial. Tales requerimientos todavía son útiles debido a que
proporcionan al programador información para mejorar la seguridad y fortaleza del
programa.
El análisis semántico estático involucra tanto la descripción de los análisis a realizar
como la implementación de los análisis utilizando algoritmos apropiados. En este
sentido, es semejante al análisis léxico y sintáctico. En el análisis sintáctico, por
ejemplo, utilizamos gramáticas libres de contexto en la Forma Backus-Naus (BNF, por
sus siglas en inglés) para describir la sintaxis y diversos algoritmos de análisis
sintáctico descendente ascendente para implementar la sintaxis. En el análisis
semántico la situación no es tan clara, en parte porque no hay un método estándar
(como el BNF) que permita espeficar la semántica estática de un lenguaje, y en parte
porque la cantidad y categoría del análisis semántico estático varía demasiado de un
lenguaje a otro. Un método para describir el análisis semántico que los escritores de
compiladores usan muy a menudo con buen efectos es la identificación de atributos,
o propiedades, de entidades del lenguaje que deben calcularse y escribir ecuaciones
de atributos o reglas semánticas, que expresan cómo el cálculo de tales atributos
está relacionado con las reglas gramaticales del lenguaje. Un conjunto así de atributos
y ecuaciones se denomina gramática con atributos.
Las gramáticas con atributos son más útiles para los lenguajes que obedecen el
principio de la semántica dirigida por sintaxis, la cual asegura que el contenido
semántico de un programa se encuentra estrechamente relacionado con su sintaxis.
Todos los lenguajes modernos tienen esta propiedad. Por desgracia, el escritor de
compiladores casi siempre debe construir una gramática con atributos a mano a partir
del manual del lenguaje, ya que rara vez la da el diseñador del lenguaje. Aún peor, la
construcción de una gramática con atributos puede complicarse innecesariamente
debido a su estrecha adhesión con la estructura sintáctica explícita del lenguaje. Un
fundamento mucho mejor para la expresión de los cálculos semánticos es la sintaxis
abstracta, como se representa mediante un árbol sintáctico abstracto. Incluso. el
diseñador del lenguaje, también suele dejar al escritor del compilador la
especificacición de la sintaxis abstracta.
Los algoritmos para la implementación del análisis semántico tampoco son tan
claramente expresables como los algoritmos de análisis sintáctico. De nuevo, esto se
debe en parte a los mismos problemas que se acaban de mencionar respecto a la
especificación del análisis semántico. No obstante, existe un problema adicional
causado por la temporización del análisis durante el proceso de compilación. Si el
análisis semántico se puede suspender hasta que todo el análisis sintáctico (y la
construcción de un árbol sintáctico abstracto) esté completo, entonces la tarea de
implementar el análisis semántico se vuelve considerablemente más fácil y consiste
en esencia en la especificación de orden para un recorrido del árbol sintáctico. junto
con los cálculos a realizar cada vez que se encuentra un nodo en el recorrido. Sin
embargo, esto implica que el compilador debe ser de paso múltiple. Si. por otra parte,
el compilador necesita realizar todas sus operaciones en un solo paso (incluyendo la
generación del código), entonces la implementación del análisis semántico se convierte
en mucho más que un proceso a propósito para encontrar un orden correcto y un método
para calcular la información semántica (suporiiendo que un orden correcto así exista en
realidad). Afortunadamente, la práctica moderna cada vez más permite al escritor de
compiladores utilizar pasos múltiples para simplificar los procesos de análisis semántico y
generación de código.
A pesar de este estado algo desordenado del análisis semántico, es muy útil para estudiar
gramáticas con atributos y cuestiones de especificación. ya que esto redundará en la
capacidad de escribir un código más claro, más conciso y menos proclive a errores para
análisis semántico, además de permitir una comprensión más fácil de ese código.
Por lo tanto, el capítulo comienza con un estudio de atributos y gramáticas con atributos.
Continúa con técnicas para implementar los cálculos especificados mediante una
gramática con atributos, incluyendo la inferencia de un orden para los cálculos y los
recorridos del árbol que los acompañan. Dos secciones posteriores se concentran en las
áreas principales del análisis semántico: tablas de símbolos y verificación de tipos. La
última sección describe un analizador semdntico para el lenguaje de programación TlNY .
ATRIBUTOS Y GRAMATICAS CON ATRIBUTOS
Un atributo es cualquier propiedad de una construcción del lenguaje de programación.
Los atributos pueden variar ampliamente en cuanto a la inf«rmación que contienen, su
complejidad y en particular el tiempo que les torna realizar el proceso de
traducci<ín/ejec~icióncu ando pueden ser determinados. Ejemplos típicos de atributos son
El tipo de <latos de una variable El valor de una expresión La ubicaciún de una variable
en la memoria El cbdigo objeto de un procedimiento El número de dígitos significativos en
un número Los atributos se pueden csrablecer antes del proceso de compilaciún (o
incluso la construcción de un con~pilador).P or ejemplo, el número de dígitos significativos
eri un número se puede establecer (o por lo menos dar un valor mínimo) mediante la
definición de un Ienguaje.
Además, los atributos s6lo se pueden determiriar durante la ejecuciúti tiel progrini;~, ti11
como el valor de una expresicín (no consiante). o la ubicaci6n de una estructura {le datos
dininiicaniente asignada. El proceso (te calcular un atributo y asociar su valor calculado
con la construcciún del lenguaje en cuestión se define corno fijación del atrihuio. El
tiempo que toma el proceso de compilación cuando se presenta la fijación de un atributo
se enomina tiempo de fijación. Los tiempos de fijación de atributos diferentes varían, e
incluso el mismo atributo puede tener tiempos de fijación bastante diferentes de un
lenguaje a otro.
Definición de otro autor:
Análisis sintáctico, semántico y generación de código
Como lo hemos mencionado antes, la sintaxis trata con la forma de los programas válidos,
mientras que la semántica trata con su significado. Se dice que la sintaxis de un lenguaje es
aquella porción del mismo que puede ser descrita de manera adecuada por medio de una
Gramática Libre de Contexto (GLC), mientras que la semántica es aquella porción del
lenguaje que no puede ser descrita de esta manera.
El análisis semántico y la generación de código intermedio pueden ser descritos en
términos de la anotación o decoración de un árbol sintáctico o árbol de análisis sintáctico
(parse tree). Las anotaciones de un árbol sintáctico son llamados “atributos”.
El analizador sintáctico recibe una serie de tokens del analizador léxico y los organiza en
un árbol sintáctico. La manera en que estos tokens son organizados está definida por un
conjunto de reglas, potencialmente recursivas. Este conjunto de reglas es la gramática libre
de contexto.
En general, una GLC consiste de:



Un conjunto finito N de símbolos no terminales,
Un conjunto finito T de símbolos terminales,
Un subconjunto finito P de [(N U T)* - T*] x (N U T)* llamado conjunto de
producciones, y

Un símbolo inicial σ Є N.
La tarea del analizador sintáctico (parser) es determinar si una cadena de entrada puede
derivarse a partir del símbolo inicial, y si es así, cómo puede hacerse. Hay dos maneras
básicas de hacer esto:


Análisis sintáctico descendente (top-down): puede verse como el intento de
encontrar la derivación más a la izquierda de un flujo de entrada recorriendo el árbol
sintáctico de arriba hacia abajo. Los tokens son analizados de izquierda a derecha.
Para manejar la ambigüedad se suele utilizar lo que se conoce como opción
inclusiva, expandiendo los lados derechos de las reglas gramaticales. Los
analizadores LL (Left-to-right, Leftmost derivation) y Recursivos-descendentes son
ejemplos de este tipo de análisis.
Análisis sintáctico ascendente (bottom-up): el analizador inicia con la secuencia de
entrada e intenta re-escribirla hasta llegar al símbolo inicial. Intuitivamente el
analizador intenta localizar el elemento más básico, después los elementos que
contienen éstos, y así sucesivamente. Los analizadores LR (Left-to-right, Rightmost
derivation) son ejemplos de este tipo de analizadores, que también son conocidos
como Analizadores Desplaza-Reduce (Shift-Reduce).
Analizadores LL vs. LR



El acrónimo LL viene de Left-to-Right, Leftmost Derivation, lo que significa que
cuando se encuentra una regla de producción no terminal que contiene componentes
no terminales en su parte derecha, el componente no terminal de más a la izquierda
es sustituido por el lado derecho de su respectiva regla. Si esa regla también
contiene componentes no terminales, el proceso se repite.
Debido a este comportamiento, los árboles sintácticos generados por los
analizadores LL tienden a crecer más hacia la izquierda que hacia la derecha. Para
tratar de evitar ambigüedades, el analizador siempre lee el siguiente símbolo en la
cadena de entrada, y lo llama “look-ahead”. Por otro lado, el analizador LL decide
la regla de producción a utilizar, dependiendo de lo que resta por analizar en la
cadena de entrada. No mantiene información sobre el progreso del análisis. Si surge
alguna ambigüedad que no pueda ser resuelta con el auxilio del símbolo “lookahead”, el analizador produce un error. Aunque es posible aumentar el número de
símbolos “look-ahead”, el problema esencial persiste; además de los símbolos
“look-ahead”, no hay otro método para recuperarse de ambigüedades en este tipo de
analizadores.
Los analizadores LR (Left-to-right, Rightmost derivation) también utilizan
símbolos “look-ahead”, pero además mantienen información sobre el estado del
proceso de análisis. Para este registro utilizan una pila (stack) donde guardan
información del estado inmediato anterior. Cada regla de producción es considerada
un estado y cada regla que resulta en un símbolo no terminal es considerada una

transición a otro estado. Esto hace que los analizadores LR sean más resistentes a
las ambigüedades que los LL, pero pueden resultar en analizadores que requieren
más recursos (en términos de tiempo de procesamiento y espacio de memoria).
Los analizadores LALR (Look Ahead LR) son una especialización de los
analizadores LR. Pueden manejar más GLC que el LR básico. Es un tipo de
analizador muy popular porque logra un buen balance entre el número de
gramáticas que puede manejar y su consumo computacional (en términos de
procesador y memoria). Las herramientas Yacc y Bison generan analizadores
sintácticos de este tipo.
Ejemplo de análisis sintáctico
Para ilustrar estos conceptos, supongamos que tenemos un lenguaje de expresiones
algebraicas escritas con los símbolos T = {x, y, z, +, *, (, )}, que constituyen los símbolos
terminales de la gramática. Los símbolos no-terminales que utilizaremos son E para una
expresión y T para términos que constan de factores (símbolo no-terminal F), es decir,
N={E,T,F}.
Una expresión algebraica puede consistir de un solo término:
E→T
O la suma de una expresión y un término:
E→E+T
Un término puede consistir de un factor, o del producto de un factor y un término:
T→F
T→F*T
Un factor puede consistir de una expresión algebraica entre paréntesis o de uno de los
símbolos terminales:
F→(E)
F→x
F→y
F→z
Estas reglas, también llamadas producciones, definen el conjunto de todas las cadenas
válidas de este lenguaje.
Una expresión válida para este lenguaje es x * y + z * (x + y). El árbol sintáctico
correspondiente a esta expresión es el siguiente:
Figura 1. Árbol sintáctico de la expresión algebraica x * y + z * (x + y)
Gramática de atributos
Cuando describimos un lenguaje por medio de una GLC, no se puede decir nada acerca de
los significados de las expresiones. Para el manejo de los significados es común usar una
gramática de atributos, la cual asigna un valor a cada símbolo (terminal y no-terminal) del
lenguaje. La gramática se incrementa con un conjunto de reglas de producción que
especifican como se relaciona cada símbolo con su valor.
Para ilustrar el uso de las gramáticas de atributos, supongamos la siguiente gramática de
expresiones algebraicas compuesta por constantes y que maneja precedencia y
asociatividad:
E→E+T
E→E-T
E →T
T→T*F
T→T/F
T→F
F → -F
F→(E)
F → constante
Ahora presentamos la gramática de atributos para este lenguaje de expresiones algebraicas:
1. E1→ E2 + T
» E1.val := suma(E2.val, T.val)
2. E1→ E2 - T
» E1.val := diferencia(E2.val, T.val)
3. E → T
» E.val := T.val
4. T1→ T2 * F
» T1.val := producto(T2.val, F.val)
5. T1→ T2 / F
» T1.val := cociente(T2.val, F.val)
6. T → F
» T.val := F.val
7. F1→ -F2
» F1.val := inverso(F2.val)
8. F → ( E )
» F.val := E.val
9. F → constante
» F.val := constante.val
Como vemos, en estas reglas de la gramática de atributos, asignar significados a las
sentencias del lenguaje equivale a asignar una función que recibe como parámetros los
componentes de la sentencia y regresa un valor que se asigna a esa expresión.
Resumen
Hemos visto hasta ahora que el análisis sintáctico y el análisis semántico utilizan algunas
herramientas matemáticas para realizar sus funciones: árboles sintácticos y gramáticas de
atributos. Estas herramientas nos permiten definir la sintaxis y semántica de un lenguaje y
nos preparan para la generación de código intermedio.
UNIDAD 2 Generación
código intermedio.
de
Competencia específica de la unidad:
Aplicar las herramientas para desarrollar una máquina virtual que ejecute código
intermedio a partir del código fuente de un lenguaje prototipo.
CONTENIDO TEMATICO
Código máquina
La aparición de los lenguajes de alto nivel mejoró la productividad de los programadores al
liberarlos de los detalles de bajo nivel de la computadora que estaban programando. El
compilador es quien se encarga de realizar la conversión del código fuente en lenguaje de alto
nivel al código máquina de la computadora que ejecutará el programa. Es, por tanto, el
compilador quien ahora maneja los detalles de bajo nivel de la arquitectura a programar. Esto
implica que el desarrollo del compilador requiere del conocimiento de los detalles de la
arquitectura de la máquina destino. En este apartado conoceremos la computadora abstracta para
la que Tiny traduce sus programas.
3.2.1 La máquina abstracta
En este apartado describimos la computadora para la cual el compilador de Tiny genera
código objeto. Está diseñada para facilitar la traducción de un lenguaje similar al Pascal a
una arquitectura abstracta. Esta computadora es un subconjunto de la computadora Dream
(Dream machine) diseñada por Frank DeRemer. Se trata de una computadora orientada a
pila (stack) sin registros direccionales, por lo que carece de instrucciones como “carga
registro X con …”; es decir, todas las instrucciones suponen que los datos están en la pila y
guardan los resultados en la pila.
Además, se han eliminado algunas restricciones normales en las máquinas reales:


Las instrucciones de ramificación no están limitadas a un determinado rango.
Todas las instrucciones tienen el mismo formato, y no dos o tres formatos
diferentes, como en la arquitectura IA32, por ejemplo.
La computadora tiene tres memorias diferentes y separadas entre sí:



De código
De datos
De direcciones de retorno
Los límites de cada tipo de memoria están indicados por una serie de registros. La siguiente
figura ilustra estas memorias y sus registros límite:
La memoria de código se considera de sólo-lectura. Ningún programa puede saltar (branch)
fuera de su área de código, ya que todas sus instrucciones están confinadas al rango de
direcciones limitado por los registros CBR y CLR. Ni el código ni las direcciones de
retorno pueden modificarse ni copiarse, debido a que todas las lecturas y escrituras a
memoria están restringidas a realizarse entre los registros GBR (Global Base Register) y
STR (Stack Top Register).
En otras palabras, en esta máquina es imposible ejecutar código que se automodifique.
El límite de la memoria disponible está determinado por el registro SLR (Stack Limit
Register). Este registro simula el tamaño limitado de la memoria física de las computadoras
reales; sin embargo, para fines del compilador de Tiny, supondremos que la máquina no
tiene limitación de memoria, es decir, el contenido de SLR es “infinito”.
Todo el direccionamiento de las variables globales y locales es relativo a GBR (Global
Base Register) o a LBR (Local Base Register), según corresponda. Por tanto, la instrucción
“LLV i” (Load Local Value i) significa “coloca en el tope de la pila (push) el valor de la iésima palabra del marco local (local frame). Por tanto, i puede verse como un
desplazamiento (offset) a partir de LBR. La ejecución de esta instrucción ajustará el
registro del tope de la pila (STR). Otro ejemplo es “SGVi” (Storage Global Value i), que
significa “saca de la pila el valor en el tope (pop) y almacénalo en la i-ésima palabra
global”. De nuevo, la ejecución de esta instrucción ajustará el STR, e i es el desplazamiento
respecto de GBR. Otro ejemplo más es “LIT i” (Literal i), que significa “coloca i en la
pila”.
Con el fin de simplificar, asumimos que la ejecución del programa, una vez cargado en
memoria, inicia en la instrucción indicada en CBR; es decir, asumimos que el cargador
carga el programa a partir de esa localización. La herramienta TWS incluye un intérprete
que hace las veces de la máquina abstracta.
3.2.2 Formato de instrucciones
El formato de las instrucciones de la máquina abstracta es el siguiente:
{etiqueta} [mnemónico de la instrucción] [0, 1 o 2 operandos]
Donde la etiqueta es opcional, además vemos que hay instrucciones que no manejan datos,
otras que manejan un sólo dato y otras que manejan dos datos. Es decir, en el conjunto de
instrucciones encontramos instrucciones con 0, 1 o 2 campos de direccionamiento.
3.2.3 Conjunto de instrucciones
Las instrucciones que la máquina abstracta puede ejecutar son las siguientes:
De control del programa
NOP : Hacer nada
HALT : Detenerse (halt)
De transferencia de datos
LIT v : Cargar (Push) la Literal v en Local frame (Lf) de la memoria de datos.
LLV i : Cargar valor local (Load Local Value) i en Lf.
LGV i : Cargar valor global (Load Local Value) i en Lf.
SLV i : Almacena valor local i (Store Local Value i) en Lf.
SGV i : Almacena valor global i.
LLA i : Cargar Dirección Local i (Load Local Address i) en Lf.
LGA i : Cargar Dirección Global i (Load Global Address i) en Lf.
POP n : Extrae n valores (Pop n values).
DUP : Duplica el tope de la pila.
SWAP : Intercambia los dos valores superiores de la pila.
Aritméticas y lógicas
UOP i : Operación unitaria i (Unary Operation i): const X = Pop Lf.
Push (Unop(i,X)) en Lf
BOP i : Operación binaria i: const Xr,Xl = Pop Lf, Pop Lf
Push (Binop(i,Xl,Xr)) en Lf
De control de flujo
CALL n : Llamada a una subrutina.
RTN n : Retorno de rutina.
GOTO L : I <- L Salto incondicional a L
COND L M: I <- if Pop Lf = True # Pop Stack. Si el valor es:
then L # Verdadero, salta a L
else M # Falso, salta a M.
fi
CODE F : Push F on Lf # Carga el punto de entrada.
SOS i : Llamada a la función i del sistema operativo
Donde
Unop(i,X) significa:
case i of
UNOT : not(X)
UNEG : -(X)
USUCC : X+1
UPRED : X-1
Binop(i,Xl,Xr) significa:
case i of
BAND : Xl and Xr
BOR : Xl or Xr
BPLUS : Xl + Xr
BMINUS : Xl - Xr
BMULT : Xl * Xr
BDIV : Xl div Xr
BMOD : Xl mod Xr
BEQ : Xl = Xr
BNE : Xl <> Xr
BLE : Xl <= Xr
BGE : Xl >= Xr
BLT : Xl < Xr
BGT : Xl > Xr
Llamadas al sistema
Para simplificar el manejo de los dispositivos de Entrada/Salida suponemos que tenemos
disponibles algunas llamadas al sistema operativo, a las cuales se accede por medio de la
instrucción SOS:
SOS (Sistema operativo) i significa:
case i of
TRACEX : Trace_Execution <- not TraceExecution
DUMPMEM: Dump_Memory
INPUT : readln(i)
Push i on Lf
INPUTC : readln(ch)
Push Ord(ch) on Lf
OUTPUT : write (Pop Lf)
OUTPUTC: write (Chr(Pop(Lf)))
OUTPUTL: writeln
EOF : if eof(input)
then Push True on Lf
else Push False on Lf
Vemos que la máquina abstracta puede ejecutar las cuatro operaciones aritméticas básicas:
suma, resta, multiplicación y división, además la operación módulo; también puede ejecutar
las tres operaciones lógicas básicas: NOT, ANDy OR; puede ejecutar saltos condicionales e
incondicionales. Cuenta con llamadas al sistema operativo para las operaciones de
Entrada/Salida y para funciones de depuración.
Como sabemos, el compilador debe traducir del lenguaje de alto nivel a este conjunto de
instrucciones. Es, por tanto, necesario entender como utilizar este lenguaje ensamblador
para escribir programas en bajo nivel para la máquina abstracta. A continuación
presentamos un par de ejemplos de traducción de Tiny al ensamblador de la máquina
abstracta.
Ejemplo 1
Vamos a mostrar la traducción del siguiente programa en Tiny:
Program copy:
{ Despliega en pantalla (eco) los primeros 10 números leídos con input }
Var count: integer;
Begin
Count:=1;
While( count <= 10) do
Begin
Output(read);
Count := count + 1;
End
End copy.
El código máquina para este programa se muestra abajo. Obviamente, los comentarios no
son generados por el generador de código.
LIT 0 # Espacio para Count
LIT 1 # Asigna 1
SGV 0 # Count := 1
L2 LGV 0 # Carga Count
LIT 10 # Carga 10
BOP BLE # POP, Compara. Push resultado T/F
COND L3 L4 # Pop stack, si V, ve a L3, sino, ve a L4.
L3 SOS INPUT # Llamada al SO. Lee y coloca en la pila
SOS OUTPUT # Llamada al SO. Saca de la pila y despliega
SOS OUTPUTL # Despliega un avance de línea
LGV 0 # Carga Count
LIT 1 # Carga 1
BOP BPLUS # Súmalos y guarda resultado en la pila
SGV 0 # Saca de la pila y almacena en Count
GOTO L2 # Regresa a L2
L4 HALT # Salta aquí cuando termines. Deténte.
Ejemplo 2
Con el fin de ayudar al entendimiento de esta máquina estudiaremos otro programa
ejemplo, escrito en el lenguaje “Medium”, que es el lenguaje intermedio para Tiny:
program fact:
var m: integer;
function fact(n: integer) : integer
begin
If n > 0 then
fact := n * fact( n – 1 );
else
fact := 1;
m := m + 1;
end;
begin
m := 0;
output( fact( read ), m );
end fact.
El código para la máquina abstracta es el siguiente:
LIT 0
GOTO L1
L2 LLV 1
LIT 0
BOP BGT
COND L3 L4
L3 LLV 1
LIT 0
LLV 1
LIT 1
BOP BMINUS
CODE L2
CALL 3
BOP BMULT
SLV 0
GOTO L5
L4 LIT 1
SLV 0
NOP
L5 LGV 0
LIT 1
BOP BPLUS
SGV 0
LLV 0
RTN 1
L1 LIT 0
SGV 0
LIT 0
SOS INPUT
CODE L2
CALL 1
SOS OUTPUT
LGV 0
SOS OUTPUT
SOS OUTPUTL
HALT
Es muy recomendable que traces con papel y lápiz la ejecución de estos dos programas de
ejemplo para que entiendas mejor la programación en bajo nivel de la máquina abstracta.
UNIDAD 3 Optimización.
Competencia específica de la unidad:
Conocer e Identificar los diferentes tipos de optimización que permita eficientar el
código intermedio.
CONTENIDO TEMATICO
Estrategias de optimización
La optimización del código es el proceso de "puesta a punto" (tuning) de la salida de un
compilador con el fin de minimizar o maximizar algún atributo del programa objeto. Es
común que la meta de la optimización sea minimizar el espacio en memoria que ocupa del
código o maximizar la velocidad de ejecución.
Una forma de clasificar las estrategias de optimización del código se basa en el alcance de
la optimización: desde una sola instrucción (optimización local), hasta un programa entero
(optimización global).
Técnicamente hablando es más sencillo implementar estrategias locales que globales,
aunque normalmente el beneficio es menor que el obtenido con estrategias globales.
Algunos ejemplos de estrategias de optimización clasificadas por su alcance son:



Optimizaciones locales: Sólo toman en cuenta la información local de una función.
Requieren menos tiempo de análisis.
Optimización interprocedural o de programa completo: Requieren analizar el
programa completo. Un ejemplo de estrategia de este tipo es el reemplazo de una
llamada a función con el código de la función, lo que reduce la sobrecarga normal
de una llamada a función.
Optimización de lazos: Se aplica sólo a las estructuras repetitivas, como for() y
while(). Este tipo de optimización puede tener un fuerte impacto porque muchos
programas dedican un buen porcentaje de su tiempo de ejecución a este tipo de
instrucciones.
Además de la clasificación por su alcance, las estrategias de optimización se dividen en dos
categorías generales:


Independiente vs. dependiente del lenguaje de programación. La optimización
independiente del lenguaje de programación se basa en el hecho de que muchos
lenguajes de programación comparten estructuras comunes. La optimización
dependiente del lenguaje trata de aprovechar las particularidades de cada lenguaje
de alto nivel.
Independiente vs. dependiente de la máquina destino. Muchas de las mejores
optimizaciones se logran explotando las características particulares de la plataforma
destino.
Obviamente las características de la plataforma destino también afectan las estrategias de
optimización. Entre estas características tenemos las siguientes:



Número de registros internos en el CPU.
Tipo de procesador: RISC vs. CISC.
Número de unidades funcionales: coprocesador numérico o no, pipelining, etcétera.
Para evitar complicar mucho al compilador de Tiny, la herramienta TWS no maneja ningún
tipo de optimización; sin embargo, muchos compiladores sí lo hacen, ya que la posibilidad
de obtener código optimizado siempre es un atractivo más para cualquier compilador,
independientemente del lenguaje a traducir.
Detección de errores
Los errores de programación pueden clasificarse en tres categorías:



Errores de compilación
Errores de ejecución
Errores lógicos
Errores de compilación
Son errores que aparecen cuando se está compilando el programa y eventualmente
ocasionan que se detenga el proceso de traducción.
Muchos de estos errores son causados por equivocaciones de captura, por ejemplo, un mal
deletreo de alguna palabra clave, mezcla incorrecta de mayúsculas y minúsculas en los
nombres de variables, o la falta de un signo de puntuación, entre otros.
Son los errores más fáciles de corregir porque contamos con la ayuda del compilador.
Cuando el compilador detecta un error durante el analizador sintáctico o no puede asignar
significado a una expresión durante el análisis semántico, genera un mensaje de error
mostrando una breve descripción del error y la línea del código fuente donde lo encontró.
Normalmente el compilador intenta seguir el proceso de traducción con el fin de detectar la
mayor cantidad posible de errores e informarlos al programador para que proceda a
corregirlos.
Cuando aparecen varios errores de compilación, por lo general sólo el primer error es real,
los demás pueden no serlo.
Errores de ejecución
Son errores que aparecen mientras el programa está en ejecución.
Un ejemplo típico es la división entre cero; por ejemplo, supón que tienes la siguiente
expresión:
velocidad = kilómetros / horas
Si la variable horas = 0 cuando se intente ejecutar la instrucción, la división no puede
calcularse y se genera un error de ejecución.
Errores lógicos
Causan que el programa no funcione correctamente y que arroje resultados equivocados.
En este tipo de errores la compilación termina sin errores y el programa se ejecuta sin
errores de ejecución, pero arroja resultados no esperados, éstos son los más difíciles de
encontrar y corregir.
PRACTICA: Agregando la multiplicación al traductor
inicial
En este apartado vamos a ilustrar la manera de expandir el lenguaje inicial agregando la
multiplicación de dos enteros. Esto requiere modificar los siguientes componentes del
traductor:




El analizador léxico (lex.tiny)
El analizador sintáctico (parse.tiny)
El analizador semántico (constrainer)
El generador de código (gencode)
Vamos a ver como se realiza este proceso:
Cambios al analizador léxico lex.tiny
Las reglas para las operaciones dentro del analizador léxico son las siguientes:
"+"
{
"-" { returnrule(yytext[0]); }
returnrule(yytext[0]);
Para agregar el operador "*" se debe agregar la siguiente línea:
"*" { returnrule(yytext[0]); }
Cambios al analizador sintáctico parser.tiny
}
A continuación reproducimos la parte del analizador sintáctico que define las operaciones
aritméticas:
Expression ->Term
->Term LTE Term => "<=";
Term ->Primary
->Primary '+' Term => "+";
Primary -> '-' Primary => "-"
-> READ => "read"
->Name
->INTEGER_NUM => "<integer>"
-> '(' Expression ')';
Name -> IDENTIFIER => "<identifier>";
Para introducir el operador de la multiplicación, se debe agregar un nuevo productor de
factor:
Expression ->Term
->Term LTE Term => "<=";
Term ->Primary
->Primary '+' Term => "+";
Factor ->Primary
->Primary '*' Factor => "*";
Primary -> '-' Primary => "-"
-> READ => "read"
->Name
->INTEGER_NUM => "<integer>"
-> '(' Expression ')';
Name -> IDENTIFIER => "<identifier>";
Modificaciones al constrainer
El constrainer consiste de la clase tws::constrainer y del archivo de cabecera nodes.h, que
definen los nodos del árbol que el constrainer y el generador de código deben recorrer para
realizar su trabajo. El archivo nodes.h contiene las siguientes líneas:
addnode(ProgramNode,"program");
addnode(TypesNode, "types");
addnode(TypeNode, "type");
addnode(DclnsNode ,"dclns");
addnode(DclnNode, "dcln");
addnode(IntegerTNode,"integer");
addnode(BooleanTNode, "boolean");
addnode(BlockNode,"block");
addnode(AssignNode, "assign");
addnode(OutputNode, "output");
addnode(IfNode ,"if");
addnode(WhileNode ,"while");
addnode(NullNode , "null");
addnode(LENode ,"<=");
addnode(PlusNode ,"+");
addnode(MinusNode,"-");
addnode(ReadNode,"read");
addnode(IntegerNode,"<integer>");
addnode(IdentifierNode,"<identifier>");
Se debe agregar la siguiente línea para el nodo de la multiplicación:
addnode(MultNode ,"*");
El archivo de la clase tws::constrainer tiene el método expression(), que analiza las
expresiones en el AST y determina si están bien formadas. A continuación mostramos el
fragmento de este método que verifica los nodos "+" y "-":
if ((nodename == PlusNode) or (nodename == MinusNode)){
Type1 = expression(T->get_child(0));
if(T->get_degree()==2){
Type2 = expression(T->get_child(1));
}else{
Type2 = TypeInteger;
}
if( (Type1 != TypeInteger) or (Type2 != TypeInteger)){
error(T);
cout<< "ARGUMENTS OF '+', '-' etc. MUST BE OF TYPE\
INTEGER" <<endl;
}
returnTypeInteger;
}
Para manejar el nuevo nodo de multiplicación "*", se debe agregar el siguiente código al
método:
if (nodename == MultNode){
Type1 = expression(T->get_child(0));
Type2 = expression(T->get_child(1));
if( (Type1 != TypeInteger) or (Type2 != TypeInteger)){
error(T);
cout<< "ARGUMENTS OF '*', MUST BE OF TYPE\
INTEGER" <<endl;
}
returnTypeInteger;
}
Modificaciones al generador de código
El código para las expresiones aritméticas es generado en la clase tws::codegenerator, en el
método expression(). A continuación mostramos el fragmento de este método que genera el
código para el nodo "-":
if (name == MinusNode){
expression(T->get_child(0),CurrLabel);
if (T->get_degree() == 2){
expression(T->get_child(1),NoLabel);
codegen(NoLabel,BOPOP,BMINUS);
dec_framesize();
}else{
codegen(NoLabel,UOPOP,UNEG);
}
}
El código para el nodo "*" debe generarse de la siguiente manera:
if (name == MultNode){
expression(T->get_child(0),CurrLabel);
expression(T->get_child(1),NoLabel);
codegen(NoLabel,BOPOP,BMULT);
dec_framesize();
}
Donde BMULT es la instrucción en ensamblador para la multiplicación.
Recompilando el traductor
Una vez realizados todos los cambios anteriores en sus respectivos archivos fuente, es
necesario recompilar nuestro traductor. Esto lo hacemos por medio de la utilería make, es
decir, estando ubicados en el directorio tws simplemente tecleamos:
make
Probando la multiplicación
El último paso es capturar y compilar un programa que utilice la nueva instrucción, por
ejemplo:
{Archivo: mult.tiny }
programmulti:
var factor1,factor2, producto : integer;
begin
factor1 := read;
factor2 := read;
producto = factor1 * factor2;
output(producto)
endmulti.
Recuerda que compilas el programa con la instrucción:
tc mult.tiny
UNIDAD 4 Generación
código objeto.
del
Competencia específica de la unidad:
Utilizar un lenguaje de bajo nivel para traducir el código construido a lenguaje máquina
para su ejecución.
Análisis del código objeto a crear
La función del compilador es traducir un programa escrito en un lenguaje fuente (un
lenguaje de alto nivel) a un programa equivalente en lenguaje máquina del procesador
destino. Este programa está escrito en código objeto. En esta sección del Módulo 4 veremos
cómo obtener el código objeto que se genera a partir de algunas instrucciones en lenguaje
fuente.
A manera de recordatorio, a continuación están reproducidas las instrucciones en lenguaje
ensamblador de la máquina abstracta. Toda instrucción del lenguaje fuente debe traducirse
a una o varias de estas instrucciones.
Las instrucciones que la máquina abstracta puede ejecutar son las siguientes:
De control del programa
NOP
HALT : Detenerse (halt)
:
Hacer
nada
De transferencia de datos
LIT v : Cargar (Push) la Literal v en Local frame (Lf) de la memoria
LLV
i
:
Cargar
valor
local
(Load
Local
Value)
i
LGV
i
:
Cargar
valor
global
(Load
Local
Value)
i
SLV i : Almacena valor local i (Store Local Value i)
SGV
i
:
Almacena
valor
global
LLA i : Cargar Dirección Local i (Load Local Address i)
LGA i : Cargar Dirección Global i (Load Global Address i)
POP
n
:
Extrae
n
valores
(Pop
n
DUP
:
DUPlica
el
tope
de
la
SWAP : Intercambia los 2 valores superiores de la pila.
de datos.
en
Lf.
en
Lf.
en Lf.
i.
en Lf.
en Lf.
values).
pila.
Aritméticas y lógicas
UOP i : Operación unitaria i (Unary Operation i): const X = Pop Lf.
Push
(Unop(i,X))
en
Lf
BOP i : Operación binaria i: const Xr,Xl = Pop Lf, Pop Lf
Push (Binop(i,Xl,Xr)) en Lf
De control de flujo
CALL
n
:
Llamada
a
una
subrutina.
RTN
n
:
Retorno
de
rutina.
GOTO
L
:
I
<L
Salto
incondicional
a
L
COND L M : I <- if Pop Lf = True # Pop Stack. Si el valor es:
then
L
#
Verdadero,
salta
a
L
else
M
#
Falso,
salta
a
M.
fi
CODE
F
:
Push
F
on
Lf
#
Carga
el
punto
de
entrada.
SOS i : Llamada a la función i del sistema operativo
Para las operaciones aritméticas y lógicas tenemos que:
Unop(i,X)
case
UNOT
UNEG
USUCC
UPRED : X-1
i
:
:
:
significa:
of
not(X)
-(X)
X+1
Binop(i,Xl,Xr)
case
BAND
BOR
BPLUS
BMINUS
BMULT
BDIV
BMOD
BEQ
BNE
BLE
BGE
BLT
BGT : Xl > Xr
:
:
:
:
:
:
:
:
:
:
:
:
i
Xl
Xl
Xl
Xl
Xl
Xl
Xl
Xl
Xl
Xl
Xl
Xl
and
or
+
*
div
mod
=
<>
<=
>=
<
significa:
of
Xr
Xr
Xr
Xr
Xr
Xr
Xr
Xr
Xr
Xr
Xr
Xr
Llamadas al sistema
Para simplificar el manejo de los dispositivos de Entrada/Salida suponemos que tenemos
disponibles algunas llamadas al sistema operativo, a las cuales se accede por medio de la
instrucción SOS:
SOS i significa:
case
i
TRACEX
:
Trace_Execution
DUMPMEM:
INPUT
:
Push
i
INPUTC
:
Push
Ord(ch)
OUTPUT
:
write
OUTPUTC:
write
OUTPUTL:
EOF
:
then
Push
True
else Push False on Lf

<-
not
on
on
(Pop
if
on
of
TraceExecution
Dump_Memory
readln(i)
Lf
readln(ch)
Lf
Lf)
(Chr(Pop(Lf)))
writeln
eof(input)
Lf
Código generado por el compilador de Tiny
Cuando compilamos un programa de Tiny se generan varios archivos a partir del programa
fuente; estos archivos son:

_CONS: Creado por el analizador semántico, contiene comentarios sobre el
cumplimiento de las restricciones de Tiny.



_CGEN: Creado por el generador de código, con comentarios sobre la traducción.
Contiene además la traducción de las instrucciones del programa fuente,
enriquecido con algunos comentarios útiles para entender mejor el proceso de
traducción.
_TREE: Creado por el analizador sintáctico, contiene el árbol sintáctico obtenido a
partir del programa fuente.
_CODE: Creado por el generador de código, contiene el programa objeto, sin
comentarios.
Es importante anotar que estos archivos se sobrescriben cada vez que compilo un programa
en Tiny. Esto es útil porque me permite analizar el código generado por el compilador de
Tiny para el último programa traducido, y se evita el uso inmoderado del espacio en disco.
A manera de ejemplo, vamos a ver el código generado por el compilador de Tiny para el
programa de prueba p7, que se muestra a continuación:
program
begin
output(3)
end x.
x:
Recuerda que para compilar este programa debes ejecutar el programa Cygwin. El
comando para compilar este archivo, esta ubicado en el directorio tiny, es:
./tc tests/p7
En la siguiente figura se muestra el resultado de la compilación y ejecución de p7.
También se pueden observar los comandos "cd" para posicionarme en el directorio de tiny:
A continuación te muestro el contenido del archivo _CGEN creador por el compilador de
Tiny:
<<< CODE
<<< CODE
<<< CODE
<<< CODE
<<< CODE
LIT
Incrementing
SOS
Decrementing
SOS
HALT
GENERATOR >>> Processing Node program ,
GENERATOR >>> Processing Node dclns ,
GENERATOR >>> Processing Node block ,
GENERATOR >>> Processing Node output ,
GENERATOR >>> Processing Node <integer> ,
Framesize
to
Framesize
to
Label
Label
Label
Label
Label
is
is
is
is
is
3
1
OUTPUT
0
OUTPUTL
Recuerda que el generador de código recorre el árbol sintáctivo generado por el analizador
sintáctico y revisado por el analizador semántico, para ir generando el código objeto.
Observa que el primer nodo analizado es el nodo program, después el nombre del
programa, que es un nodo decln, posteriormente un nodo block (bloque), cuyo inicio es
indicado por el begin, después una instrucción de salida (ouput), que recibe como
parámetro un nodo entero (3, en este caso). Esta instrucción output(3) se traduce por las
instrucciones lit 3, sos output y sos outputl. Posteriormente, la instrucción end x.que se
traduce por halt.
El contenido del archivo _CODE es el siguiente:
LIT
SOS
SOS
HALT
3
OUTPUT
OUTPUTL
Fácilmente puede verse que es el mismo código objeto que aparece en el archivo _CGEN,
pero sin comentario alguno. Este archivo nos muestra el contenido del archivo ejecutable
que se "cargará" al intérprete de la máquina abstracta para su ejecución.

Ejemplo del código generado por el compilador de Tiny para una estructura
selectiva
Para ilustrar el código generado por el compilador de Tiny para una estructura selectiva,
vamos a analizar brevemente el código generado por el siguiente programa:
program
begin
if(
1
end ejemplo2.
ejemplo2:
<=
2
)
then
output(1)
else
output(2)
El contenido del archivo _CGEN es el siguiente:
<<< CODE GENERATOR >>> Processing Node program
<<< CODE GENERATOR >>> Processing Node dclns
<<< CODE GENERATOR >>> Processing Node block
<<<
CODE
GENERATOR
>>>
Processing
Node
if
<<<
CODE
GENERATOR
>>>
Processing
Node
<=
<<< CODE GENERATOR >>> Processing Node <integer>
LIT
Incrementing
Framesize
to
,
Label
Label
Label
,
Label
,
Label
, Label
,
,
is
is
is
is
is
is
1
1
<<< CODE GENERATOR
LIT
Incrementing
BOP
Decrementing
COND
Decrementing
<<< CODE GENERATOR
<<< CODE GENERATOR
L1
Incrementing
SOS
Decrementing
SOS
GOTO
<<< CODE GENERATOR
<<< CODE GENERATOR
L2
Incrementing
SOS
Decrementing
SOS
NOP
L3 HALT
>>>
Processing
Node
Framesize
Framesize
L1
Framesize
>>> Processing Node
>>> Processing Node
LIT
Framesize
Framesize
>>> Processing Node
>>> Processing Node
LIT
Framesize
Framesize
<integer>
,
Label
is
2
to
2
BLE
to
1
L2
to
0
output , Label is L1
<integer> , Label is L1
1
to
1
OUTPUT
to
0
OUTPUTL
L3
output , Label is L2
<integer> , Label is L2
2
to
1
OUTPUT
to
0
OUTPUTL
Observa el orden del recorrido del árbol sintáctivo en el archivo _CGEN:






Node program: Siempre debe iniciar con este nodo.
Node dclns: Y continuar con éste, por el nombre del programa.
Node block: Inicio del begin.
Node if: En este ejemplo, primera instrucción del programa.
Node <= : Operador relacional de la condición del if.
Node <integer>: Entero 1, etcétera.
El código ejecutable de este programa, que es el contenido del archivo _CODE, es el
siguiente:
LIT
LIT
BOP
COND
L1
SOS
SOS
GOTO
L1
LIT
1
2
BLE
L2
1
OUTPUT
OUTPUTL
L3
L2
SOS
SOS
NOP
L3 HALT
LIT
2
OUTPUT
OUTPUTL
Que realiza las siguientes operaciones:
1. Coloca el número 1 en el tope de la pila (LIT 1).
2. Coloca el número 2 en el tope de la pila (LIT 2).
3. Realiza la operación binaria (con dos operandos) BOP BLE (Branch if less or equal,
salta si es menor o igual), que compara los dos datos en el tope de la pila.
4. Si la condición es verdadera (COND L1 L2), salta a L1, si no salta a L2.
5. L1: Coloca el 1 en el tope de la pila (LIT 1) y llama a la función output (SOS
OUTPUT) del SO, para desplegar el tope de la pila, llama a outputl (SOS
OUTPUTL), para desplegar un avance de línea y salta a L3 (GOTO L3), con lo que
termina la ejecución del programa.
6. L2: Coloca el 2 en el tope de la pila (LIT 2) y llama a la función output del SO
(SOS OUTPUT), para desplegar el tope de la pila, llama a outputl (SOS
OUTPUTL), para desplegar un avance de línea, ejecuta nada (NOP) y termina
(HALT) la ejecución del programa.

Ejemplo del código generado por el compilador de Tiny para una estructura
repetitiva
Para analizar brevemente el código generado por el compilador de Tiny para una estructura
repetitiva, vamos a revisar brevemente el código generado para el siguiente programa:
program
var
begin
while
begin
output(i);
i
end;
end mientras.
mientras:
i:integer;
i
:=
<=
i
5
+
do
1;
El código generado para este programa es el siguiente:
LIT
L1
LIT
BOP
COND
LGV
L2
0
0
5
BLE
L3
L2
SOS
SOS
LGV
LIT
BOP
SGV
GOTO
L3 HALT
LGV
0
OUTPUT
OUTPUTL
0
1
BPLUS
0
L1
Donde:
1. LIT 0 es la instrucción que utiliza el compilador de Tiny para definir una variable
(en este caso la variable entera i).
2. LGV 0 (que en este ejemplo está etiquetada con L1), es la instrucción para referirse
a esa primera variable y cargarla en el tope de la pila.
3. Posteriormente carga el número 5 en el tope de la pila (LIT 5) y compara con la
operación BOP BLE (Salta si menor o igual). Si el contenido de la variable i es
menor o igual a 5, entonces salta a L2, si no salta a L3 (COND L2 L3).
4. L2: Carga i en el tope de la pila (LGV 0), llama a output para que se despliegue en
pantalla (SOS OUTPUT) y despliega un "avance de línea" llamando a outputl (SOS
OUTPUTL).
5. Carga i en el tope de la pila (LGV 0) y el 1 en el tope de la pila (LIT 1).
6. Realiza la operación suma con los dos datos en el tope de la pila y guarda el
resultado en la variable i (BOP BPLUS).
7. Salta incondicionalmente a L1 (GOTO L1), que es la verificación de la condición
del while().
8. L3: Termina (halt).
Tomando como base estos ejemplos, realiza ahora la actividad correspondiente a esta
sección del Módulo 4: Diseñar el código que deben generar cada una de las instrucciones
que agregarás al compilador de Tiny:





Operación módulo.
Operación autoincremento.
Operación autodecremento.
Estructura for().
Estructura repeat ... until().
Tarea. Aumentando la gramática del lenguaje tiny
Instrucciones: Lee detenidamente estas instrucciones y resuelve los ejercicios que se te
indican.
NOTA: En caso de que tengas alguna duda, hazla llegar al facilitador.
Ponderación: 15%
Actividad 1
Elabora una investigación documental electrónica y bibliográfica acerca de la definición
formal de la gramática de los lenguajes de programación, y agrega a la gramática inicial
de Tiny las definiciones de las instrucciones:





Módulo
Autoincremento
Autodecremento
Estructura for ()
Estructura repeat ... until
Para realizar esta actividad:
1. Lee detenidamente los contenidos del Tema 4.1, Proposiciones del lenguaje a
implementar.
1. Con base en la lectura y para reafirmar tus conocimientos y poder continuar
tus actividades, contesta las preguntas de investigación.
2. ¿Cómo se especifica formalmente la operación módulo (i%j)?
3. ¿Cómo se especifica formalmente la operación autoincremento (x++)?
4. ¿Cómo se especifica formalmente la operación autodecremento (x--)?
5. ¿Cómo se especifica formalmente la estructura for()?
6. ¿Cómo se especifica formalmente la estructura repeat ... until()?
Actividad 2
Diseñar el código que deben generar cada una de las instrucciones que agregarás al
compilador de Tiny:
Módulo Autoincremento, Autodecremento, Estructura for() , Estructura repeat ... until
Para realizar esta actividad:
1. Lee detenidamente los contenidos del Tema 4.2, Análisis del código objeto a crear.
2. Con base en la lectura y para reafirmar tus conocimientos y poder continuar tus
actividades, contesta las preguntas de investigación.
1. ¿Cómo se especifica formalmente la operación módulo (i%j)?
2.
3.
4.
5.
f.
¿Cómo se especifica formalmente la operación autoincremento (x++)?
¿Cómo se especifica formalmente la operación autodecremento (x--)?
¿Cómo se especifica formalmente la estructura for()?
¿Cómo se especifica formalmente la estructura repeat ... until()?
Definición formal de la gramática de las instrucciones indicadas.
BIBLIOGRAFIA
1. Aho, Sethi, Ullman. Compiladores Principios, técnicas y herramientasEd.
Addison Wesley.
2. Lemone Karen A. , Fundamentos de compiladores Cómo traducir al
lenguaje de computadora, Ed. Compañía Editorial Continental.
3. Kenneth C. Louden. Construcción de compiladores Principios y práctica.Ed.
Thomson.
4. Martin John, Lenguajes formales y teoría de la computación, ED. Mc Graw
Hill
5. Hopcroft John E., Introducción a la Teoría de Autómatas, Lenguajes y
Computación, ED. Addison Wesley
6. Guerra Crespo. Hector. Compiladores. Ed. Tecnologica didáctica.
7. Ronald Mak. Writing compilers and interpreters. Ed. Wiley Computer
Publishing.
8. Fischer, LeBlanc. Crafting a compiler with C. Ed. Cummings Publishing
Company, Inc.
9. Salas Parrilla, Jesús. Sistemas Operativos y Compiladores. McGraw Hill.
10. Beck. Software de Sistemas, Introducción a la programación de Sistemas.
Addison-Wesley Iberoamericana.
11. Teufel, Schmidt, Teufel. Compiladores Conceptos Fundamentales.
Addison-Wesley Iberoamericana.
12. C. Louden, Kenneth. Lenguajes de programación Principios y práctica.
Thomson.
13. Levine Gutiérrez, Guillermo. Computación y programación moderna
Perspectiva integral de la informática. Pearson Educación.
14. Abel, Peter. Lenguaje ensamblador y programación para PC IBM y
compatibles. Pearson Educación.
15. Mak, Ronald. Writing compilers and interpreters. Wiley Computer
Publishing.
16. Pittman, Thomas, Peters, James. The art of compiler design Theory and
practice. Prentice Hall.
17. Temblay & Sorenson. Compilers Writing. Mc Graw Hill.
18. R. Levine, John; Mason, Tony, Brown, Doug. Lex y Yacc. O'Reilly &
Associates.
19. The Lex & Yacc Page, 3-mar-04, 12:45, http://dinosaur.compilertools.net
20. A compact guide to lex & Yacc, Thomas Niemann, 3-Mar-04, 12:50,
http://epaperpress.com/lexandyacc
21. Lex & Yacc HOWTO, Bert Hubert (PowerDNS.COM.BV), 3-Mar-04, 12:55,
http://ds9a.nl/lex_yacc
22. Flex, 3-mar-04, 13:02, http://www.gnu.org/software/flex/flex.html
23. Compiler construction using flex and Bison, Anthony Aaby, 3-mar-04, 13:05,
http://cs.wwc.edu/aabyan/464/BooK/
24. Flex, version 2.5 A fast scanner generator, Edition 2.5, March 1995, Vern
Paxson, 3-mar-04, 13:10,
http://www.cs.princelon.edu/appel/modern/c/software/flex/flex_toc.html
25. Bison. The Yacc-compatible Parser Generator, November 1995, Bison
Version 1.5, Charles Donnelly and Richard Stallman, 3-mar-04, 13:10,
http://www.cs.princelon.edu/appel/modern/c/software/bison/bison_toc
.html, 13/dic/2009
26. Bison. http://3d2f.com/programs/30-170-microprocessor-emulator-andassemblerdownload.shtml, 13/dic/2009
27. 2/Ago/2005 ,Microprocessor Emulator and Assembler 3.10-k,
http://software.intel.com/en-us/articles/all/1/, 24/feb/2010
28. Intel, 31/dic/2009, Intel® Software Development EmulatorBottom of Form,
http://software.intel.com/en‐us/articles/intel‐software‐development‐emulator/,
24/feb/2010
Descargar