Está en la página 1de 172

UNIVERSIDAD DEL CONO SUR DE LAS AMÉRICAS

TESIS DE GRADO DE LA CARRERA INGENIERÍA EN INFORMÁTICA

TEMA: Creación de un Traductor Pseudocódigo a Lenguaje C (P2C)

REALIZADO POR
Francisco Javier Gray Benítez

Tutor: Prof. Lic. Julio César Suárez Ortega

Año 2008
Asunción – Paraguay
DEDICATORIA

A mis padres y hermanos…


AGRADECIMIENTO

Al Prof. Lic. Julio Suárez, quien más que un tutor, es un gran maestro y ha estado
brindándome su apoyo desde incluso mucho antes de emprender está empresa.
.
RESUMEN

El propósito del presente trabajo de investigación y desarrollo, fue


la de crear un traductor de Lenguaje Pseudocódigo (español estructurado) a
Lenguaje C. Para la construcción del traductor inicialmente se escribió una
gramática libre de contexto, la cual sirvió de base para la construcción de la
misma, a través de la utilización de metacompiladores fue generado un código
equivalente en Lenguaje C, el cual posteriormente se compiló a fin de producir el
traductor final. Al traductor se lo dotó de la capacidad de traducir (del Lenguaje
Pseudocódigo al Lenguaje C) tipos de datos primitivos, estructuras condicionales,
estructuras de repetición, entradas y salidas de datos, funciones propias del
lenguaje y funciones definidas por el usuario, incluyendo las combinaciones
posibles que de ellas puedan surgir, aportando de esta manera una herramienta que
facilite el proceso de aprendizaje de las técnicas de algorítmicas a todas aquellas
personas que se inician en el mundo de la programación.
INDICE DE TABLAS
Tabla 1 - Tabla de análisis sintáctico LL(1) ........................................................... 47
Tabla 2 - Tabla de Análisis LR............................................................................... 55
Tabla 3 - Reconocimiento de id*id+id.................................................................... 56
Tabla 4 - Tabla de análisis LR(1) para S'→ S$ ...................................................... 68
Tabla 5 - Tabla de análisis LALR(1) para S'→ S$ ................................................. 71
Tabla 6 – Operadores aritméticos admitidos por el traductor P2C......................... 148
Tabla 7 - Operadores relacionales admitidos por el traductor P2C ........................ 148
Tabla 8 - Operadores lógicos admitidos por el traductor P2C ............................... 148
Tabla 9 – Operador de asignación admitido por el traductor P2C ......................... 149
INDICE DE FIGURAS
Figura 1 - Evolución de los lenguajes de programación .......................................... 21
Figura 2 - Esquema Básico de un Traductor ........................................................... 22
Figura 3 - Entrada y salida de un traductor ............................................................. 27
Figura 4 - Funcionamiento del enlazador ............................................................... 27
Figura 5 - Etapas de un traductor............................................................................ 28
Figura 6 - Funcionamiento de un analizador léxico ................................................ 31
Figura 7 - Procesos de las dos fases primeras del análisis ....................................... 32
Figura 8 - Árbol de análisis sintáctico para E=m*c^2. ............................................ 36
Figura 9 - PRIMEROS y SIGUIENTE ................................................................... 47
Figura 10 - Esquema Sinóptico de un analizador LR .............................................. 54
Figura 11 - Conjunto LR(0) canónico para E' → ⋅ E ............................................... 59
Figura 12 - Diagrama AFD para los prefijos viables ............................................... 60
Figura 13 - Grafo de transiciones para los goto de S'→ S$ .................................... 68
Figura 14 - AFD Simplificado para LALR(1)......................................................... 70
Figura 15 - Interacción de etapas con la Tabla de Símbolos.................................... 75
Figura 16 - Árbol y GDA para char x char → pointer(integer) ............................... 81
Figura 17 - Representación de las expresiones de tipos .......................................... 85
Figura 18 - Posición del generador de códigos en un traductor ............................... 88
Figura 19 - Árbol de análisis sintáctico para 4*5+6 ................................................ 90
Figura 20 - Árbol de análisis sintáctico con atributo heredados .............................. 91
Figura 21 - Grafo de dependencias ......................................................................... 93
Figura 22 - Grafo de dependencias circular ............................................................ 95
Figura 23 - Momento en que se procede a traducir un programa P2C ................... 162
Figura 24 - Salida de la traducción en lenguaje C por pantalla .............................. 162
Figura 25 - Pantalla de Tc-Lite, previa a la compilación ....................................... 164
Figura 26 – Compilación del programa C generado .............................................. 165
Figura 27 – Compilación del programa C generado .............................................. 165
Figura 28 - Ejecución del programa compilado en C(parte 1) ............................... 166
Figura 29 - Ejecución del programa compilado en C(parte 2) ............................... 166
INDICE
CAPITULO I - INTRODUCCION ...................................................................... 8
CAPITULO II - MARCO TEORICO................................................................. 21
II.1 TRADUCTORES ........................................................................................ 21
II.1.1. Origen ..................................................................................................... 21
II.1.2. ¿Qué es un traductor? .............................................................................. 22
II.1.3. Tipología de los traductores según su utilización ..................................... 22
II.1.4. Fases de una traducción compilada .......................................................... 26
II.1.5. Etapas de la construcción de un traductor ................................................ 28
II.2 - ETAPA DE ANÁLISIS............................................................................. 30
II.2.1. Análisis léxico (Scanner) ......................................................................... 30
II.2.2. Análisis Sintáctico (Parser) ...................................................................... 35
II.1.3. Análisis Semántico .................................................................................. 74
II.3 - ETAPA DE SINTESIS .............................................................................. 87
II.3.1 Generación de código ............................................................................... 87
II.4 – DEFINICIÓN DIRIGIDA POR SINTAXIS.............................................. 88
II.4.1. Definición dirigida por sintaxis por medio de gramáticas con atributos .... 88
II.4.2. Grafos de dependencias ........................................................................... 92
II.4.3. Esquemas de traducción........................................................................... 96
CAPITULO III - MARCO METODOLOGICO ................................................. 97
III.1. Descripción de la profundidad y Diseño de la tesis .................................... 97
III.2. Cómo se realizó la tesis ............................................................................. 98
III.3. Descripción de los instrumentos y procedimientos utilizados para la
recolección y tratamiento de la información. ...................................................... 99
III.4. Descripción de la muestra utilizada. .......................................................... 99
III.5. Adecuación de los métodos a los objetivos de la tesis. ............................... 99
III.6. Tratamiento de los datos.......................................................................... 100
CAPITULO IV - RESULTADOS .................................................................... 101
IV.1. Código fuente de los Analizadores Sintáctico y Semántico del traductor
P2C.................................................................................................................. 101
IV.2. Tabla de símbolos en Lenguaje C del traductor P2C. ............................... 135
IV.3. Código fuente del Analizador Léxico del traductor P2C. ......................... 142
IV.4. Manual de programador del traductor P2C. ............................................. 144
CAPITULO V – CONCLUSIONES ................................................................ 167
CAPITULO VI –RECOMENDACIONES ....................................................... 170
BIBLIOGRAFIA ............................................................................................. 171
CAPITULO I - INTRODUCCION

Desde los comienzos de la informática, se ha buscado la manera


mas práctica de interactuar con el ordenador, y hacer que éstos realicen las
acciones tal y como se desea, con resultados en tiempo y forma.

Con el pasar de las generaciones han ido surgiendo técnicas cada


vez más sofisticadas, las cuales han dado como resultado toda una nueva rama de
la informática, la de los traductores. Así en el año 1952 Grace Murray Hopper
(1906-1992), dio origen al primer traductor, cuyo objetivo era el de traducir las
instrucciones escritas en inglés al lenguaje máquina de un ordenador.

Tal y como el trabajo de Murray Hopper, el propósito de este


trabajo de investigación y desarrollo, fue crear un traductor de lenguajes que
permita traducir programas escritos en LENGUAJE PSEUDOCODIGO a
LENGUAJE C.

Pensando en quienes se inician en la codificación de programas


para computadoras, han surgido las siguientes cuestiones, ¿existen herramientas
sencillas, que permitan realizar comprobaciones de la eficacia de los algoritmos?,
¿responden a estas necesidades los lenguajes actuales del mundo computacional?

Exceptuando al Lenguaje Basic en sus versiones iniciales,


actualmente no existen otras herramientas fáciles de acceder, como el desarrollado
por medio de la presente tesis. Es pues esto, una respuesta a las necesidades de
contar con un programa sencillo de utilizar, con un lenguaje de programación
próximo al lenguaje natural, el cual simplifica la complejidad del estudio de la
algorítmica.

8
El traductor desarrollado facilita la traducción de un lenguaje de
uso académico (sin aplicación práctica a otros ámbitos) a un lenguaje de porte
científico-comercial de amplia aplicación y utilización mundial, cual es el
lenguaje C.

El desarrollo fue realizado entre enero de 2006 y febrero de 2007,


tiempo en que se ha trabajado en el diseño, programación, pruebas y puesta a
punto.

Para la realización de la tesis fueron fijados los siguientes


objetivos:

Objetivo General:
Desarrollar un traductor de códigos teniendo como origen el
Lenguaje Pseudocódigo y como destino el Lenguaje de C.

Objetivos Específicos:
1. Diseñar las construcciones sintácticas de un lenguaje
Pseudo-codificado, en español, de fácil utilización para el
usuario.
2. Diseñar y desarrollar las analizadores léxicos y sintácticos
para la verificación de los programas fuentes.
3. Diseñar y desarrollar un sistema de tipos de datos, para la
implementación de un comprobador de tipos, que permita
la verificación del programa fuente, en concordancia con la
tipificación básica del lenguaje objeto.
4. Diseñar y desarrollar la comprobación estática de unicidad
y flujo de control.
5. Diseñar y desarrollar el generador del código objeto.

El presente material inicia con el primer capitulo desarrollando una


introducción al tema, el segundo capitulo expone paso a paso el marco teórico, el
tercer capitulo aborda la metodología seguida para la realización del presente

9
trabajo de tesis, en el capitulo cuatro se exponen los resultados obtenidos, en el
quinto capitulo las conclusiones a las que se ha arribado por medio del presente
trabajo de tesis, y finalmente, en el capitulo sexto se establecen unas series de
recomendaciones para aquellas personas que deseen proseguir el tema abordado
por la presente tesis.

10
GLOSARIO DE TÉRMINOS:

A
ADA: Lenguaje de programación estructurado y fuertemente tipado de forma
estática que fue diseñado por Jean Ichbiah de CII Honeywell Bull por encargo
del Departamento de Defensa de los Estados Unidos.

Ad-hoc: Es una hipótesis creada expresamente para explicar un hecho que


contradice una teoría.

Alfabeto: También conocida como vocabulario; es un conjunto finito, no-vacío de


símbolos (objetos atómicos o indivisibles), se denota con la letra griega Σ
(Sigma).

Ambigüedad: Es cuando una gramática genera para una misma cadena de


entrada dos árboles de análisis distintos.

Ámbito: Contexto en cual una declaración tiene validez. P.e., un programa, un


trozo de programa.

Árbol de derivación: Es una representación gráfica de como se deriva una forma


sentencial a partir del símbolo no-terminal inicial.

Árbol universal: Es aquel que tiene como raíz al axioma inicial y representa
todas las derivaciones posibles a partir de cualquier forma sentencial.

Asociatividad: Estable el orden de evaluación de las operaciones (asociatividad


por izquierda, asociatividad por derecha).

Atributo: Es cada una de las características o propiedades de una entidad o


interrelación.

Atributo Sintetizado: Es llamado atributo sintetizado a un atributo cuyo valor es


determinado por los valores de sus atributos hijos, en un proceso de recorrido del
árbol de derivación de abajo para arriba (Bottom-up).

Atributo Heredado: Es aquel atributo cuyo valor es calculado a partir de los


valores de sus hermanos y su padre.

Autómata finito: También conocida como maquina de estado finito; es un modelo


matemático de un sistema que recibe una cadena constituida por símbolos de un
alfabeto y determina si esa cadena pertenece al lenguaje que el autómata
reconoce.

Autómata finito de pilas (AFP): Son maquinas abstractas construidas para


reconocer los lenguajes regulares.

11
Autómata finito determinista (AFD): Es aquel autómata finito cuyo estado de
llegada está unívocamente determinado por el estado inicial y el carácter leído
por el autómata.

Autómata finito no determinista (AFN): Es aquel que presenta cero, una o más
transiciones por el mismo carácter del alfabeto

Axioma inicial: Es el no-terminal inicial de la cadena de entrada.

B
Backpatching: También conocida como relleno de retroceso; es una técnica de
generación de código, utiliza banderas para marcar las posiciones para valores
desconocidos, para posteriormente completarlas (paching).

Base de datos: También conocida como banco de datos; es un conjunto de datos


que pertenecen al mismo contexto, almacenados sistemáticamente para su
posterior uso.

BNF: Acrónimo de Backus Naur Form (en español, Forma Normal de Backus);
es una metasintaxis usada para expresar gramáticas independientes del contexto.
Es una manera formal de describir lenguajes formales.

Buffer: Es una ubicación de la memoria en una computadora, ó en un


instrumento digital, reservada para el almacenamiento temporal de información
digital mientras que está esperando ser procesada.

C
C: Es un lenguaje de programación creado en 1969 por Ken Thompson y Dennis
M. Ritchie en los Laboratorios Bell como evolución del anterior lenguaje B, a su
vez basado en BCPL. Al igual que B, es un lenguaje orientado a la
implementación de Sistemas Operativos, concretamente Unix. C es apreciado por
la eficiencia del código que produce y es el lenguaje de programación más
popular para crear software de sistemas, aunque también se utiliza para crear
aplicaciones.

Cadena de entrada: Es el conjunto de caracteres proporcionados como entrada


de un traductor para su análisis y traducción posterior si fuera el caso.

Cierre: Conjunto de todas las posibles cadenas finitas sobre un alfabeto, se


denota como Σ*.

Clase: También conocida como clausura ó Clausura de Kleene; son


declaraciones o abstracciones de objetos, lo que significa, que una clase es la

12
definición de un objeto. Cuando se programa un objeto y se definen sus
características y funcionalidades, realmente se programa una clase.

Código de máquina: Es una codificación de programas en sistema binario que es


el único que puede ser directamente ejecutado por una computadora.

Comando: Del inglés command; es una instrucción o mandato que el usuario


proporciona al sistema.

Compilador: Es un programa informático que traduce un programa escrito en un


lenguaje de programación a otro lenguaje de programación, generando un
programa equivalente que la máquina será capaz de interpretar.

Condición de seguridad: Es una herramienta de la se dota a los lenguajes a fin


de realizar una evaluación previa a la ejecución de una aplicación, la cual
terminará ejecutándose, si y solo si satisface la condición de seguridad.

Constructor de tipos: Método o función propia de un lenguaje computacional,


tiene como tarea principal la definición.

Contexto: Son las circunstancias bajo las cuales un dispositivo ó programa está
siendo utilizado.

Corrutina: Es aquella porción de código que funciona subordinada a otra.

Costos: Es el tiempo en el cual se incurre durante un proceso dado. P.e.: Costos


de CPU. Costos de E/S.

CPU: Acrónimo de Central Processing Unit; es la unidad central de


procesamiento, ó simplemente, el procesador. En una computadora digital, es el
componente que interpreta las instrucciones y procesa los datos contenidos en los
programas.

Criterios de poda: Se define como todo aquél criterio que permite saber, dada
una solución parcial, que no va a ser posible obtener a partir de ellas soluciones
válidas.

D
Derivación: Secuencia de sustituciones de no-terminales que, partiendo de un
símbolo inicial (denotado con S), produce un resultado (denotado con ω).

Dirección de memoria: Es una colección de casilla ó celdas que almacena datos


e instrucciones, identificadas unívocamente por una serie de números.

13
DOS: Acrónimo de Disk Operating System (Sistema Operativo de Disco), creado
originalmente para las computadoras IBM PC, las cuales utilizaban procesadores
8086/8088 de 16 bits.

E
E/S: Acrónimo de entrada/salida, del inglés input/output (I/O); es el conjunto de
dispositivos físicos utilizados por el sistema para comunicarse unas con otras,
como para comunicar al sistema con las personas.

Encapsulamiento: Se denomina encapsulamiento al ocultamiento del estado, es


decir, de los datos miembros de un objeto, de manera que sólo se puede cambiar
mediante las operaciones definidas para ese objeto.

Enlazador: Del inglés, linker; programa que a partir de ficheros objetos


generados en los primeros procesos de traducción, quita todos los recursos no
precisados y enlaza el código objeto con las bibliotecas, para finalmente producir
un código ejecutable ó biblioteca.

EOF: Acrónimo de end-of-file; es un indicador o marca de que no hay mas


información que recuperar de una fuente de datos.

Épsilon (E ε): Es la quinta letra del alfabeto griego. En el sistema de numeración


griega tiene un valor de 5. En matemáticas, ε suele designar a pequeñas
cantidades, o cantidades que tienden hacia cero, en particular en el estudio de los
límites o de la continuidad.

Estructura de datos: Conjunto de datos en que sus componentes están


relacionados entre sí de una forma particular y sobre los que se pueden realizar
ciertas operaciones según sea el tipo de relación que hay entre ellos.

F
Fichero: También conocido como archivo; es un conjunto de información que se
almacena en algún medio de escritura que permita ser leído o accedido por una
computadora.

Fichero Ejecutable: También conocido como archivo ejecutable; es aquel cuyo


contenido se interpreta por el ordenador como un programa.

Fichero Fuente: También conocido como archivo fuente; es un conjunto de


códigos escritos en texto simple, capaz de ser leído por cualquier editor de texto,
en el, están dispuestos todas las tareas que deberá realizar la computadora.

14
Fichero Objeto: También conocido como archivo objeto; contiene al conjunto de
caracteres en código de maquina o bytecode, resultado de la compilación del
fichero fuente.

G
Gramática: Ciencia que estudia los elementos de una lengua y sus
combinaciones.

H
Herencia: Es uno de los mecanismos de la programación orientada a objetos,
por medio del cual una clase se deriva de otra, llamada entonces superclase, de
manera que extiende su funcionalidad.

I
Instrucciones: Conjunto de reglas o advertencias necesarias para ejecutar una
tarea, un trabajo, un juego, etc.

Inteligencia Artificial: Ciencia que intenta la creación de programas para


máquinas que imiten el comportamiento y la comprensión humana.

Interpretes: Es un programa capaz de analizar y ejecutar otros programas


escritos en un lenguaje de alto nivel, realizando la traducción a medida que sea
necesario, típicamente, instrucción por instrucción, y normalmente no guardan el
resultado de dicha traducción, por lo que la ejecución de programas en lenguajes
interpretados suelen ser bastante lentos.

Invocación: Se denomina invocación al acto de solicitar la ejecución de un


programa, rutina o subrutina.

J
Java: 1- Del Indonesio Jawa; es la isla más poblada del archipiélago indonesio,
donde se sitúa la capital, Yakarta, muy conocida también por su café. 2-
Tecnología desarrollada por Sun Microsystems para aplicaciones de software
independientes de la plataforma (lleva el nombre de Java en honor al café
producido en dicha Isla, de ahí que el símbolo de dicho programa sea una taza de
café).

15
L
Lenguaje: Es cualquier conjunto de cadenas finitas sobre un alfabeto.

Lenguaje de alto nivel: Es aquel lenguaje de programación en el que las


instrucciones enviadas al ordenador son similares al lenguaje humano.

Lenguaje de maquina: Es un lenguaje compuesto por un conjunto de


instrucciones que determinan acciones a ser tomadas por la máquina. Un
programa de computadora consiste en una cadena de estas instrucciones de
lenguaje de máquina (más los datos).

Lenguaje ensamblador: Lenguaje de programación que está a un paso del


lenguaje de máquina, por ende los programadores deben estar familiarizados con
la arquitectura del computador para el cual están programando, siendo los
programas en lenguaje ensamblador no documentados, y en consecuencia
difíciles de mantener, como el lenguaje ensamblador es dependiente del
hardware; hay un lenguaje ensamblador diferente para cada serie de CPU.

Lenguaje Natural: El lenguaje natural es el lenguaje hablado y/o escrito por los
seres humanos para propósitos generales de comunicación.

Lenguaje Orientado a Objetos: Es aquel lenguaje de programación capaz de


soportar Programación Orientada a Objetos.

Librería: También conocida como biblioteca; conjunto de procedimientos y


funciones (subprogramas) agrupadas en un fichero con el fin de que puedan
aprovecharlas otros programas. Existen dos tipos de librerías; 1- Estáticas (de
enlace estático) y, 2- Compartidas (de enlace dinámico).

M
Macros: Grupo de comandos de una aplicación, organizados según un
determinado juego de instrucciones y cuya ejecución puede ser pedida de una
sola vez para realizar una función deseada.

Memoria: Del inglés, Memory; es el espacio de trabajo del computador


(físicamente suelen ser una colección de chips RAM), determina el tamaño y el
número de programas que pueden ejecutarse al mismo tiempo, así como también
la cantidad de datos que puede procesarse en cada instante.

Módulo: Es un componente de un programa computacional, el cual de las varias


tareas que un programa deberá realizar para cumplir su función ú objetivo, el
módulo realizará una o varías de estas tareas.

16
N
No Terminales: Es aquel símbolo que en una derivación siempre es reemplazada
demás, pues no termina en una derivación, algunos autores tal y como es el caso
de Kenneth C. Louden, lo llaman como estructuras.

Nodos: Es una estructura ó registro que dispone de varios campos, y al menos


uno de ellos deberá ser un puntero a otro nodo, es utilizada para la realización de
lista enlazada, árbol ó grafo.

O
Operadores: Son expresiones que permiten manipular los datos que se les pasa,
cada uno de los datos que se pasa a un operador se llama operando, y según el
número de operandos de que disponga un operador estaremos hablando de un
operador unario (un operando), binario (dos operandos), ternario (tres
operandos), etc.

P
Palabras reservadas: Son palabras propias de un lenguaje de programación, las
mismas no pueden ser utilizadas como nombre de identificadores.

Parámetros: También conocida como argumento, es una información que


determina el funcionamiento de un programa informático. Los parámetros pueden
tener valores de todo tipo. Por ejemplo: números, textos, expresiones o incluso el
nombre de un archivo.

Pasadas: Es el acto de leer el código fuente por parte de un traductor.

PASCAL: Lenguaje de programación desarrollado por el Profesor Suizo Niklaus


Wirth a finales de los años 60, bajo el objetivo crear un lenguaje que facilitara el
aprendizaje de la programación a sus alumnos, sin embargo, con el tiempo este
se convirtió en una herramienta para desarrollo de aplicaciones de todo tipo.

PERL: Acrónimo de Practical Extracting and Reporting Lenguaje; es un


Lenguaje de programación de amplia difusión usado para web mediante la
programación de CGI. Muy usado para extraer información de archivos de texto
y para la generación de informes a partir de ficheros.

Pila: Es un tipo de estructura de datos en el que el último dato en llegar es el


primero en salir.

17
PL/I: Acrónimo de Programming Language 1 (Lenguaje de Programación 1); fue
propuesto por IBM hacia 1970 para responder a las necesidades de las
aplicaciones científicas y comerciales, disponible en las novedosas plataformas
de utilidad general IBM 360 y más adelante IBM 370.

Polimorfismos: Es la capacidad que tienen objetos de diferentes clases de


responder al mismo mensaje o evento.

Portabilidad: Se define como la dependencia del software respecto a la


plataforma del mismo, y es inversamente proporcional a la dependencia de
plataforma. A menor dependencia, mayor portabilidad.

Precedencia: Especifica la prioridad de cada operador respecto a otro.

Programa: Secuencia de instrucciones que realizan una tarea específica.

Programa fuente: Código del programa original, el cual es almacenada en un


fichero, conocido como fichero o archivo fuente.

Programa objeto: Un programa a nivel de lenguaje máquina que resulta de la


compilación de un programa fuente.

Programación: Es la implementación de un algoritmo en un determinado


lenguaje de programación, conformando un programa.

Programación Orientada a Objetos (POO): Es un paradigma de programación


que usa objetos y las interacciones entre estas para diseñar y desarrollar
aplicaciones, se basa en varias técnicas, como la herencia, modularidad,
polimorfismo y encapsulamiento. Popularizado recién a principio de la década de
los 1990, en la actualidad cuenta con varios lenguajes soportan la orientación a
objetos.

R
Recursividad: Propiedad de algunos lenguajes de programación de permitir que
un programa solicite su propia ejecución en el curso de su ejecución.

Rutina: Es un conjunto de código, escrito para un propósito particular, que al ser


llamado dentro de un programa el código principal de éste se detenga y se dirija
al código de la rutina invocada.

S
Shell: Es un programa informático que actúa como Interfaz entre el usuario y el
sistema operativo mediante una ventana que espera comandos textuales

18
ingresados por el usuario en el teclado, los interpreta y los entrega al sistema
operativo para su ejecución. Líneas de comandos, Intérprete de mandatos,
Intérprete de línea de mandatos, Intérprete de comandos, Terminal, Consola,
Shell ó su acrónimo en inglés CLI por Command line interface, son formas
comunes de llamarlo.

Símbolos inaccesibles: No terminales a los que no se puede llegar por medio de


derivación alguna.

Símbolos muertos: No terminales incapaces de generar una cadena de


terminales, a través de derivaciones sucesivas.

Sintaxis: Son las reglas que rigen la formulación de las instrucciones en un


programa de computación, en base a una gramática dada.

Sistema Operativo: Es un programa o conjunto de programas destinados a


permitir una gestión eficaz de recursos, éste empieza a trabar cuando se enciende
el computador, es el encargado de gestionar el hardware de la maquina desde los
niveles más básicos, permitiendo interacción con el usuario.

Sobrecarga: Es la posibilidad de tener dos o más funciones (sobrecarga de


funciones) y/u operadores (sobrecarga de operadores) con el mismo nombre pero
brindando funcionalidades distintas, el compilador utilizará una ú otra
dependiendo de los parámetros enviados.

T
Terminal: Son símbolos del alfabeto en los cuales termina una derivación.

Tiempo de ejecución: Del inglés Runtime; es el intervalo de tiempo en el que un


programa de informático se ejecuta en un sistema operativo.

Tira nula: Secuencia de vacía de símbolos.

U
Unix: Registrado oficialmente como UNIX®, es un sistema operativo portable,
multitarea y multiusuario; desarrollado, en principio, en 1969 por un grupo de
empleados de los laboratorios Bell de AT&T, entre los que figuran Ken
Thompson y Dennis Ritchie. Según Dennis Ritchie, «después de treinta años de su
creación, UNIX sigue siendo un fenómeno»

19
V
Variables: Son estructuras de datos que, como su nombre indica, pueden cambiar
de contenido a lo largo de la ejecución de un programa. Una variable
corresponde a un área reservada en la memoria principal del computador.

W
Windows: Microsoft Windows, conocido simplemente como Windows, es un
sistema operativo con interfaz gráfica para computadoras personales cuyo
propietario es la empresa Microsoft.

20
CAPITULO II - MARCO TEORICO

II.1 TRADUCTORES
II.1.1. Origen
El estudio de los traductores se remonta a los orígenes de la
informática misma, ya en 1952 Grace Murray Hopper (1906-1992), en vista a la
necesidad que tenía creo un traductor Inglés-Lenguaje Maquina, posterior a esto y
hasta nuestros días la ciencia-arte de crear lenguajes ha evolucionado
notablemente, empezando por los ensambladores que fueran creados por cada uno
de los fabricantes, con sus propias especificaciones para cada procesador, hasta
los lenguajes de alto nivel y llegando a los lenguajes orientados a objetos los
cuales trabajan a base de acciones o comportamiento acorde a los distintos
impulsos externos que reciben.

Figura 1 - Evolución de los Lenguajes de Programación.

Fuente: CUEVA 1998:10

Al principio, el desarrollo de un traductor era considerado una


tarea difícil y de alto consumo de tiempo, por lo que eran escasos los centros e
institutos de investigación que dedicaban esfuerzos en su construcción, (GALVEZ

21
et al., 2005), mencionan que, para la implementación del primer compilador
FORTRAN (Formula Translator) se necesito lo equivalente a 18 años de trabajo
individual.

Hoy día gracias a las técnicas avanzadas de desarrollo de


traductores y lenguajes, el desarrollo de un traductor puede ser realizado por un
pequeño grupo de investigación ó como trabajo de fin de carrera, tal cual el
presente estudio, en la década de 1950, tiempo en que se desarrollo el primer
compilador FORTRAN esto era toda una utopía.

II.1.2. ¿Qué es un traductor?


Un traductor se define como un programa que traduce un programa
escrito en un lenguaje (lenguaje fuente) a otro programa o lenguaje (lenguaje
destino), y como parte de este proceso el traductor debe emitir mensajes de errores
al usuario si los hubiere.

Figura 2 - Esquema Básico de un Traductor

Programa Programa
Traductor
Fuente Destino

Mensajes
de Error
Fuente: Suárez 2006:1

II.1.3. Tipología de los traductores según su utilización


Traductores de idioma: Son programas que se encargan de realizar
traducciones de un idioma a otro, este tipo de traductores deben sortear un sin fin
de dificultades, como ser:

1. Significado de las palabras según el contexto.

22
2. Necesidad de un alto nivel de Inteligencia Artificial: Debido
al hecho de que el nivel de inteligencia artificial sigue siendo pobre para lo que se
precisa a nivel de traductores de idiomas, esto se presenta como una dificultad
complicada de superar en la traducción de frases hechas, al respecto cabe
mencionar una anécdota expuesta por (GALVEZ et al., 2005), donde mencionan
que durante la guerra fría, en un intento por realizar traducciones en forma
automática del ruso al inglés y viceversa, se puso a prueba un prototipo
introduciendo un texto en inglés: “El espíritu es fuerte, pero la carne es débil”
cuya traducción al ruso se paso de nuevo al inglés para ver si coincidía con el
original, y para sorpresa de los desarrolladores el resultado fue: “El vino está
bueno, pero la carne está podrida”, esto es debido a que en inglés spirit, posee más
de un significado, según se aplique puede significar espíritu ó alcohol.

• Intérpretes: Los intérpretes tienen una estructura similar al


de los traductores compiladores, en funcionamiento, varían sustancialmente,
debido a que los intérpretes, reciben como entrada un programa fuente en lenguaje
formal y como salida realiza la ejecución del programa, esto es debido a que el
programa fuente es reconocida y ejecutada al mismo tiempo, algunos ejemplos de
lenguajes intérpretes son; SNOBOL (StriNg Oriented SimbOlyc Language), LISP
(LISt Processing), algunas versiones del BASIC (Beginner´s All-propuse
Symbolic Instruction Code).

• Preprocesadores: Los preprocesadotes se utilizan con la


finalidad de modificar el programa fuente, al respecto (AHO et al., 1990), habla
sobre las distintas funciones que estas pueden realizar, las cuales se describen a
continuación:

i. Procesamiento de macros: Permite al


programador-usuario definir macros, las cuales son abreviaturas de
construcciones muchos más grandes.

23
ii. Inclusión de ficheros: Inserta ficheros completos
dentro del programa en cual aparece en la cabecera, en la misma
línea en la que aparece se insertará su contenido antes de que el
programa fuente sea compilado, el lenguaje C es un lenguaje que
típicamente utiliza este tipo de preprocesamiento, por medio de la
cláusula #include <nombre_de_archivo>.

iii. Procesadores “racionales”: Utilizados con la


finalidad de enriquecer a los lenguajes antiguos con nuevas
funcionalidades, como los controles de flujos, nuevos tipos de
datos, interfaces con dispositivos de hardware, entre otros.

iv. Extensiones a Lenguajes: Tratan de crear


posibilidades al lenguaje al cual son insertados, por medio de
macros, en este aspecto puede considerarse el lenguaje Equel
STONEBRAKER et al. (1976, citado de AHO, 1990), el cual es un
lenguaje de consultas a base de datos integrado al lenguaje C. Las
proposiciones que inician con ##, son tomadas por el
preprocesador, sin que ellas pasen por el lenguaje C y las traduce
directamente a llamadas a la base de datos.

• Intérpretes de comandos: Un intérprete de comandos


traduce sentencias1 simple s a invocaciones de programas, ejemplos de estos
son las shell de los sistemas operativos de la familia UNIX, o la línea de comando
MS-DOS de los sistemas MS-Windows.

• Ensambladores: En los albores de la informática, tiempos en


los que todos los programas eran escritos en lenguaje de maquina, aparecieron
como el primer gran salto hacia los lenguajes de altos niveles como los que hoy
poseemos.

1
Sentencia(s): Secuencia de expresiones (terminales) que especifica una o varias operaciones.

24
Un ensamblador es un traductor sencillo, el lenguaje fuente tiene
una estructura, en la que una sentencia se traduce de manera única a una
instrucción en código de maquina, por lo que la correspondencia ensamblador-
código de maquina es una a una.

• Traductores fuente-fuente: Permite obtener mayor


portabilidad del código fuente escrito, debido a que traduce códigos fuentes
escritos para un lenguaje a otro. El presente trabajo de investigación y desarrollo
tiene como fin desarrollar un traductor fuente-fuente.

En estos tipos de traductores, es habitual que los programas fuentes


resultantes requieran de retoques manuales debidos a diversos motivos (Gálvez
Rojas et al., 2005):
i. En situaciones en las que el lenguaje destino
carece de importantes características, de las que dispone el
lenguaje origen, por ejemplo una conversión Java a C, podría
precisar retoques, debido a que el lenguaje C no dispone del
recolector de basuras (Garbage Collector).

ii. En situaciones en las que la traducción no es


inteligente y los programas destinos son ineficientes.

Un ejemplo de traductor fuente-fuente podría ser como ya se ha


mencionado anteriormente el traductor realizado por Murray Hopper en 1952, así
como el POLARIS el cual recibe como entrada programas escritos en
FORTRAN77 y los traduce en programas eficientes a fin de ser utilizados en
computación paralela, cuya salida nuevamente es FORTRAN77, aunque con
directivas explicitas que provee paralelismo.

• traductor cruzado: Es aquel que genera un código objeto


ejecutable para una maquina distinta a la que se escribe y/o realiza la compilación.

25
• Traductores de circuitos de silicios: Posee un lenguaje
fuente similar o idéntico que un lenguaje de programación convencional,
marcando diferencia, en el hecho de que las variables no representan ubicaciones
en memoria, sino señales lógicas (0 y 1) o grupos de señales de conmutación,
produciendo como salida el diseño del circuito impreso mediante técnicas
fotográficas.

• Conversores de formatos: Son programas que aplican las


técnicas de traductores de lenguajes para convertir ficheros escritos en un formato
dado a otro.

• Reconocimiento del habla: Los sistemas de reconocimientos


del habla realizan un análisis detallado del sonido, por medio de las técnicas de
traductores.

II.1.4. Fases de una traducción compilada


Todo traductor tipo compilador se dota de tres fases básicas, las
cuales son traducción, enlace (enlazador) y carga ó ejecución.

Traducción, Generalmente un traductor tipo compilador no


produce un programa o fichero ejecutable, en su lugar emite como salida
programa ó fichero objeto, que contiene informaciones relativas al código, así
como una tabla de símbolos con las variables y estructuras de tipos utilizadas en el
programa fuente.

26
Figura 3 - Entrada y salida de un traductor

Entrada: Salida:
Fichero.prg Traductor Fichero.obj

Fuente: Elaboración propia, basado en Suárez 2006:3.

Enlace, El enlazador se encarga de enlazar todos los componentes


generados en la etapa de traducción (referencias cruzadas), generar la estructura
de bloque de memoria para almacenar las variables en tiempo de ejecución y el
programa ó fichero ejecutable, incorporando rutinas adicionales precedentes de las
librerías, como las instrucciones de entrada/salida, funciones matemáticas y de
sistemas, entre otros.

Figura 4 - Funcionamiento del enlazador

Fuente: Elaboración propia, basado en Vélez 2003:4.

Cargador, Esta fase del compilador tiene como tarea, la de cargar el


fichero ejecutable (.exe), en otras palabras, lo que hace el cargador es, tomar el

27
código de maquina relocalizable producida por el enlazador, modificar las
direcciones de memorias relocalizables y ubicar las instrucciones y los datos
modificados en las posiciones apropiadas de la memoria. Dependiendo del
microprocesador, la estructura de memoria difiere, aunque lo más usual es que el
fichero .exe, esté divido en segmentos de códigos, datos, pilas, etc., al comienzo
del módulo de carga se le asigna la dirección de memoria relativa 0, siendo las
demás referencias dentro del modulo apuntadas al comienzo del mismo. Teniendo
todas las referencias a memoria en código relocalizable, la tarea de carga se
convierte en una tarea sencilla, pues sabiendo la dirección de memoria relativa en
la que será cargada el modulo (dirección x), bastará sumar dicha dirección (x) a
medida que se vaya produciendo la carga del módulo en memoria, a fin de obtener
las posiciones de memorias relativas de cada componente.

II.1.5. Etapas de la construcción de un traductor


El traductor puede ser dividido inicialmente en dos grandes etapas,
la etapa inicial ó de análisis y la etapa final ó de síntesis, la primera se refiere a
todo aquello que tenga que ver con el programa fuente e independiente del
programa objeto, en ella se incluyen los análisis léxicos, sintácticos y semánticos,
la construcción de tablas de símbolos, la generación de código intermedio y en
cierta medida la optimización de código, siendo la etapa final todo lo concerniente
a las partes del traductor que tienen directa relación con la maquina para la que se
construye, siendo completamente independientes del lenguaje fuente, en está etapa
se incluyen todo lo referente a la generación y optimización de código, manejo de
errores y operaciones con las tablas de símbolos.

Figura 5 - Etapas de un traductor


Prog. Fuente Análisis Síntesis Prog. Destino

Errores en el Errores en la
programa fuente generación de código
Fuente: Gálvez et. al 2005:12

28
El traductor como parte importante de su función, debe leer el
programa fuente, existen situaciones en las cuales, el leer una sola vez el
programa fuente, no resulta ser suficiente, como en el caso de la recursividad,
suponiendo que se tiene una función X, que hace referencia a una función Y, y
viceversa, en este caso hipotético, al leer la función X no se sabe si la función Y
está declarada ó si es que se ha hecho de forma correcta, sin embargo al leer la
función Y ya se sabrá que de antemano que X está y que ya ha cumplido con
todos los parámetros necesarios para ser utilizado, siendo la única alternativa de
solución para la invocación de la función Y desde X, el volver a pasar una vez
más(releer el código) a fin de rellenar los datos faltantes y corregir los errores, a
este proceso se lo conoce con el nombre de pasadas.

Lo deseable es tener la menor cantidad de pasadas, a fin de reducir


los costos (tiempos) de e/s, para ello se han creado bastas alternativas, como ser el
lenguaje PL/I, solo permite utilizar variables previamente declaradas y no se
puede realizar representaciones de código objeto para las construcciones de las
que se desconozcan los tipos de variables involucradas, del mismo modo otros
lenguajes permiten la utilización de construcciones GOTO a fin de saltar adelante
en el código para verificar la existencia de las referencias o variables utilizadas,
otros lenguajes utilizan la técnica backpatching (relleno de retroceso), dicha
técnica consiste que al encontrar un salto hacia delante (GOTO destino), generar
la estructura de una instrucción, con el código de operación para el GOTO y dejar
espacios en blancos para la dirección, guardando todas las instrucciones con
espacios en blanco para la dirección destino en una lista asociada con la entrada
de destino en la tabla de símbolos, los cuales son rellenados una vez que se
encuentra la instrucción (destino: MOV algo, R1).

29
II.2 - ETAPA DE ANÁLISIS
La etapa de análisis está formada por los siguientes tres componentes:
1. Análisis léxico ó scanner, se encarga de recorrer el programa
fuente, identificar y agrupar los caracteres en tokens.
2. Análisis sintáctico ó parser, agrupa los componentes léxicos de
forma jerárquica, construyendo frases gramaticales los cuales
son utilizados por el compilador en la fase de síntesis.
3. Análisis semántico, verifica el programa fuente a fin de
asegurarse que no haya errores de significado y realiza la
comprobación de tipos.

II.2.1. Análisis léxico (Scanner)


El análisis léxico es la primera fase de todo traductor, su labor
principal consiste en leer una cadena de entrada (lexema) del programa fuente a
analizar de izquierda a derecha y agruparlos en componentes léxicos (tokens), los
cuales poseen un significado propio, para realizar dicho proceso debe existir una
regla o un conjunto de reglas previamente predefinidos (patrón). Cada lexema o
cadena del programa fuente es comparada con el conjunto de patrones
(expresiones regulares).

Ejemplo de una cadena de entrada:


E=m*c^2
De esta forma los componentes léxicos se agruparían de la
siguiente manera:
1. El identificador ‘E’.
2. El símbolo de asignación ‘=’.
3. El identificador ‘m’.
4. El signo de multiplicación ‘*’.
5. El identificador ‘c’.
6. El signo exponencial ‘^’.
7. El número 2.

30
Figura 6 - Funcionamiento de un analizador léxico
E = m * c ^ 2
<ID> <ASIG> <ID> <*> <ID> <^> <NUM>.2
Asociación de
E valor al terminal
Punteros a NUM
tabla de m
Símbolos.
c

Tabla de Símbolos
Fuente: Elaboración propia, basado en Gálvez et. al 2005:18

Otras tareas desarrolladas por el analizador léxico incluyen:


• Procesar directivas del traductor.
• Introducir información preliminar en la tabla de símbolos.
• Eliminar comentarios, espacios en blancos, tabulaciones,
retorno de carro y todo aquello que no tenga significado
según las reglas del lenguaje.
• Sustituir macros.
• Llevar la cuenta de números de líneas, a fin de que al
momento de un error pueda informar en que línea se ha
producido y facilitar la recuperación.
• Informar de errores léxicos encontrados, como por ejemplo
cuando se encuentra un ‘;’ y este no forma parte del
lenguaje.
• Cuando algunos componentes léxicos requieren algo más
que una identificación, el analizador léxico primero
identifica el componente léxico, y luego lo evalúa.
• Si el lenguaje a analizar es complejo en su sintaxis, el
analizador léxico realiza primeramente una fase de examen,
y posteriormente realiza la fase de análisis propiamente
dicha.

31
Los componentes léxicos más habituales suelen ser; las palabras
reservadas, identificadores, operadores, constantes, símbolos de puntuación y
caracteres especiales. El analizador léxico aparte del token, también debe devolver
información adicional sobre el mismo, como ser el valor, si se tratará de una
constante ó la identificación si fuera un operador. Toda información adicional
necesaria es enviada a través de atributos, asociados a los símbolos de la
gramática que define la sintaxis del lenguaje fuente.

El analizador léxico reconoce palabras conforme a una gramática


regular en el cual se define un conjunto de caracteres con los que el mismo puede
trabajar, conocidos con el nombre de alfabeto, así el analizador léxico se
encuentra sometido al analizador sintáctico, actuando como una subrutina o
corrutina del mismo.

Figura 7 - Procesos de las dos fases primeras del análisis.


El control viene dado
por una gramática
(N, T, P, S)
Secuencia de Secuencia de Árbol
caracteres. Analizador terminales. Analizador sintáctico.
Léxico (scanner) sintáctico (parser)

Donde:
N: Conjunto de No Terminales.
T: Conjunto de Terminales.
P: Reglas de producción de la gramática.
S: Axioma o símbolo inicial.
Fuente: Gálvez et. al 2005:24

32
La necesidad de construir un analizador Léxico:
1. Simplicidad del diseño: Facilita el diseño y el mantenimiento
primeramente del analizador léxico y a la larga del traductor. El
límite entre ambos analizadores es una línea muy delgada, se
podría considerar que todo aquello que se vaya reconociendo
carácter a carácter sin poseer un significado semántico por si
solo forma parte del analizador léxico, por ejemplo:

Analizador léxico Analizador Sintáctico


0. A:=0;
123 Suma+=123
printf printf(“Hola mundo”);

2. Eficiencia: Debido a que gran parte del tiempo de la traducción


se invierte en la etapa de reconocimiento de componentes
léxicos, el diseño e implementación de un analizador léxico
independiente mejora notablemente el funcionamiento de un
traductor, pues se le puede aplicar técnicas de manejo de buffer
para lectura de caracteres de entrada y procesamiento de
patrones.

3. Portabilidad: Al poseer un analizador léxico independiente se


puede limitar los problemas de compatibilidad de dispositivos,
peculiaridades del alfabeto de partida y juego de caracteres
bases, lo cual hace que el resto del sistema sea ajeno a estos
problemas y como consecuencia facilita la portabilidad entre
plataformas.

4. Facilidad en reconocimiento de patrones complejos: Una razón


más por la que se suele realizar el análisis léxico por separado
es el reconocimiento de patrones complejos. Por ejemplo si se

33
requiriere realizar un proceso de prebúsqueda2, la complejidad
del diseño y de la implementación se dispararía por lo que
tratarlo dentro del analizador sintáctico sería algo
prácticamente imposible.

Se podría considerar que la implementación de un analizador


léxico posee la estructura:
(Expresión regular1) {Acción a ejecutar1}
(Expresión regular2) {Acción a ejecutar2}
. .
. .
. .
(Expresión regularn) {Acción a ejecutarn}

Donde cada acción a ejecutar describe que acción deberá realizar el


analizador léxico cuando una cadena de entrada coincida con la expresión regular
definida, finalizando casi siempre con la devolución de una categoría léxica
asociada, siendo los siguientes conceptos de fundamental importancia al momento
de estudiar la construcción de analizadores léxicos.

• Patrón: Es una expresión regular.


• Token: Categoría léxica, es directamente asociada a cada
patrón, cada token es convertida en número ó identificador
único en todo el traductor, siendo en ocasiones asociada al
mismo, valores adicionales necesarios para las fases
posteriores. Es lo que el terminal para el analizador
sintáctico.
• Lexema: Es una secuencia de caracteres que encaja con un
patrón.

2
Prebúsqueda (en ingles, Look Ahead): Es una técnica utilizada en la construcción de lenguajes y
traductores de lenguajes, el cual permite entre otras cosas saber con anticipación que valores
siguen al que se está analizando.

34
II.2.2. Análisis Sintáctico (Parser)
Un analizador sintáctico puede escribirse/diseñarse por medio de
una gramática libre de contexto (en adelante, GLC) ó por medio de la forma
normal de Backus (Backus-Naur Form, en adelante BNF), su tarea consiste en
agrupar los tokens en forma jerárquica y construir las frases gramaticales, si estas
frases son reconocidas como parte del lenguaje definido en analizador sintáctico
construye el árbol sintáctico (ver Figura 8), el cual es el punto de partida para el
analizador semántico.

El hecho de previamente formalizar la escritura del analizador


sintáctico ofrece la ventaja de poder encontrar ambigüedades, facilita la
ampliación y/o modificación del lenguaje, y al proporcionar una estructura de
lenguaje facilita la generación de código y detección de errores.

El analizador sintáctico debe informar de los errores sintácticos


que vaya encontrando e intentar recuperarse de ellos a fin de continuar la
traducción, controlar el flujo de tokens reconocidos por la fase anterior de análisis
y en ocasiones incorpora acciones semántica, con lo cual el proceso se convierte
en una traducción dirigida por sintaxis.

35
Figura 8 - Árbol de análisis sintáctico para E=m*c^2.

Proposición
de asignación

Expresión
Identificador

E *
Expresión Expresión

Identificador
^
Expresión Expresión
m

Identificador Numero

c 2
Fuente: Gálvez et. al 2005:57

De las tres fases de análisis, es en el analizador sintáctico en quien


recae la dura misión de manejar los errores.

Considerar el manejo de errores desde el momento de que se


comienza a pensar en construir un traductor es lo más sensato, pues caso contrario
en algún momento de la construcción del mismo se tendrá que hacer una vuelta
atrás para incorporarlo.

Los errores habituales pueden englobarse en los siguientes puntos:


• Léxicos, mala escritura de identificador, palabra clave u
operador.

36
• Sintácticos, expresión aritmética ó paréntesis no
equilibrados, fin de sentencia ‘;’.
• Semánticos, operador aplicado a un operando incompatible.
• Lógicos, como ser una llamada infinitamente recursiva.
• De corrección, el programa no hace lo que se esperaba.

De todos los errores citados los errores de corrección y lógicos


resultan imposibles de detectar por el traductor, el primero por ser una concepción
de la abstracción del programador y en lo que respecta a errores lógicos por el
exceso de esfuerzo computacional a fin de seguir los flujos del programa en
ejecución para averiguar en que punto donde se generó la llamada recursiva, lo
cual resulta no solo altamente costoso, si no que habitualmente imposible.

Se desea que al momento de detectar algún tipo de error un


analizador sintáctico sea capaz de:

• Informar la naturaleza del error y su localización lo más


exactamente posible.
• Una vez corregido el error continuar construyendo el árbol
sintáctico desde el punto en que se detuvo.
• Distinguir entre errores y advertencias.
• No debe ralentizar en forma significativa al compilador.

Existen diversos métodos o forma para el menaje de errores a


continuación se exponen algunos de ellos, como ser:
Ignorar el problema (en inglés, panic mode); Consiste en ignorar
todas las entradas hasta encontrar una condición de seguridad, como fin de
sentencia, a partir del mismo se prosigue con el análisis en forma normal y todos
los tokens desde que se produjo el error hasta la condición de seguridad quedan
descartados, así como los consumidos antes del mismo.

37
Recuperación a nivel de frase; Cada vez que encuentra un error
intenta corregirlo, por ejemplo si fuera el caso de una sentencia al que le falta un
punto y coma (‘;’), el traductor agrega dicho carácter y prosigue, el problema con
esto es que puede dar lugar a recuperaciones infinitas, pues al intentar corregir el
error podría producir un nuevo error y luego intentaría corregir el error que
produjo y así sucesivamente.

Reglas de producción adicionales; Se añade a la gramática del


lenguaje reglas de producción para reconocer errores comunes, lo cual da mayor
control y permite emitir finalmente mensajes de advertencia, en lugar de un
mensaje de error, pues permite detectar un error, corregirlo y proseguir.

Corrección global, Para este método al momento de detectar un


error, el analizador sintáctico solicita toda la secuencia de tokens al analizador
léxico, a fin de producir una secuencia y su correspondiente árbol sintáctico, de
forma que sea lo más parecido posible al original, pero sin errores.

Gramática utilizada por los analizadores sintácticos.


Desde el punto de vista de la escritura de un analizador sintáctico,
se puede considerar la gramática de libre contexto, si bien existen construcciones
sintácticas que no pueden generarse mediante una gramática de libre contexto, si
no por medio de una de nivel superior, como ser las construcciones del lenguaje
PERL.

Una gramática G se define por medio de los elementos (N, T, P, S),


en donde:
N: Conjunto de No Terminales.
T: Conjunto de Terminales.
P: Conjunto de reglas de Producción.
S: Axioma o símbolo inicial de la gramática.

38
Ejemplo: gramática no ambigua que reconoce las operaciones
aritméticas (suma y multiplicación).

E E+T
| T
T T*F
| F
F id
| num
| (E)

Siguiendo con la definición formal de lo que es una gramática


tenemos que:

N = {E, T, F}
T = {+, *, id, num, (, )}
P = Son las 7 reglas de la gramática.
S = E (símbolo inicial).

Una regla de producción es una regla de reescritura, en donde el


lado izquierdo de la regla (el no terminal) es sustituido por su parte derecha
(secuencia de terminales y/o no terminales), por ende se dice que una cadena α
deriva en β, y se denota α ⇒ β , cuando:

α ⇒ β con α , β e (N ∪ T) * , y siendo:
α≡σAδ
donde Α ∈ Ν ∧ σ, ττδ ∈ (Ν ∪ Τ )* ∧ Α → τ ∈ Ρ
β≡στδ

Dentro de una α puede haber varios A, lo cual da lugar a varias


derivaciones posibles, surgiendo los siguientes conceptos:
• Derivación por izquierda: Aquella en la que la reescritura
se realiza en la terminal de más a la izquierda.

39
• Derivación por derecha: Aquella en la que reescritura se
realizar en la terminal de más a la derecha.

Ejemplos de derivaciones, sobre la base de la siguiente gramática


ambigua:
Ε→Ε+Ε
Ε∗Ε
| num
| id
| (Ε )

Derivación por izquierda:


Ε ⇒ Ε + Ε ⇒ Ε ∗ Ε + Ε ⇒ id1 ∗ Ε + Ε ⇒ id1 ∗ id2 + Ε ⇒ id1 ∗ id2 + id3
↑ ↑ ↑ ↑ ↑

Derivación por derecha:


Ε ⇒ Ε + Ε ⇒ Ε ∗ Ε + Ε ⇒ id1 ∗ Ε + Ε ⇒ id1 ∗ id2 + Ε ⇒ id1 ∗ id2 + id3
↑ ↑ ↑ ↑ ↑

Tanto en caso de la derivación por izquierda como en la derivación


por derecha, el símbolo a derivar se encuentra señalado con una flecha apuntando
al mismo ( ↑ ), la cadena resultante de una derivación recibe el nombre de formas
senténciales3.

De este modo al analizar una sentencia del lenguaje se puede


obtener su árbol de análisis sintáctico, el cual a su vez obedece a una gramática y
es la representación utilizada para describir la derivación la misma. Se parte del
axioma inicial como raíz del árbol y conforme se vaya necesitando se grafica las
hojas hasta llegar a la cima o fondo del mismo.

3
Forma Sentencial: Cualquier secuencia de terminales y no terminales obtenida mediante
derivaciones a partir del axioma inicial.

40
El árbol debe poseer una sola raíz, el cual es el axioma inicial y
conforme se va avanzando las hojas del árbol se construyen, las cuales
constituyen la forma sentencial, si por alguna circunstancia la sentencia a
reconocer no perteneciera al lenguaje generado a partir de la gramática definida,
será imposible obtener su árbol de análisis sintáctico, en cambio si a partir de la
sentencia se pudiera construir más de un árbol para reconocerlo, nos encontramos
frente a un caso de gramática con ambigüedades o ambigua.

Partiendo de la generación del árbol sintáctico se podría decir que


existen dos tipos de analizadores.

Tipos de analizadores sintácticos:


1. Descendentes: Parten del axioma inicial, realizando
derivaciones sucesivas hasta llegar a reconocer la sentencia,
estos pueden ser.
a. Con retroceso.
b. Con funciones recursivas.
c. Predictivos con gramáticas LL(1).

2. Ascendentes: Parten de la sentencia de entrada, aplicando


derivaciones inversas hasta el axioma inicial, estos pueden ser:
a. Con retroceso.
b. Gramáticas LR(k).

Análisis descendente con retroceso; se parte del axioma inicial y


se aplican todas las reglas de posibles al no terminal de más a la izquierda de la
forma sentencial. Todas las sentencias de un lenguaje representan nodos del árbol
(árbol universal), el cual es producto de todas las posibles derivaciones desde el
axioma inicial, por ende cuando comprueba la pertenencia de una sentencia al
lenguaje, el árbol sintáctico de la misma está compuesto por los nodos desde el
punto actual a la raíz. Es recomendable utilizar una cláusula de cancelación

41
(criterios de poda) para los casos de búsquedas infructuosas, a fin para evitar que
el árbol se vuelva infinito en el intento de reconocer alguna sentencia no válida.

Con este método lo que se desea es buscar por medio del árbol
universal, recorriendo rama a rama, en caso de que una forma sentencial no
coincida con la sentencia analizada, la rama es desechada, en caso de coincidir, la
sentencia es aceptada y se retorna su árbol y por ultimo, si por medio de ninguna
rama se reconoce la sentencia, la misma debe ser rechazada.

El análisis descendente con retroceso no suele ser utilizado, si


quiera para el lenguaje natural, debido a que el retroceso es altamente ineficiente,
sumado a esto la existencia de varias otras técnicas de análisis más potentes y
basados en diferentes técnicas de análisis lenguajes.

Algoritmo de análisis descendente con retroceso:


Precondición: FormaSentencial= μ Α δ, con μ ∈ Τ * , Α ∈ Ν, y δ ∈ (Ν ∪ Τ )
*

AnálisisDescendenteConRetroceso(formasentencial,entrada)
Si μ ≡ ParteIzquierda(entrada) retornar Fin Si

ParaCada pi ∈ pi ∈ P / pi ≡ Α → α
i
≡ μ α i δ ≡ µ ' Α' δ ', con µ ' ∈ Τ ∗ , Α ' ∈ Ν ,
FormaSentencial y δ ' ∈ (Ν ∪ Τ )

Si µ ' ≡ entrada
¡Sentencia reconocida !
Retornar
Fin Si
AnalisisDescendenteConRetroceso(FormaSentencial´, entrada)
Si ¡Sentencia reconocida ! Retornar Fin Si
Fin Para
Fin AnálisisDescendeteConRetroceso

Para la llamada deberíamos hacerlo de la siguiente manera:

42
AnálisisDescendenteConRetroceso(AxiomaInicial,
CadenaAReconocer)

Si no ¡Sentencia reconocida !
¡¡ SENTENCIA NO RECONOCIDA !!
Fin Si

Análisis descendente con funciones recursivas; Unas de las


formas más sencillas de crear un análisis descendente con funciones recursivas
son las gramáticas en notación BNF, pues la misma permite utilizar palabras,
símbolos y sus sencillas reglas de escrituras a fin de escribir analizadores
sintácticos suficientemente potentes.

Análisis descendentes predictivos con gramáticas LL(1); En el


análisis descendente predictivo para cada símbolo no-terminal a ser analizada
existe únicamente una regla de producción que se puede aplicar. Este tipo de
análisis está basada en gramáticas LL(1), donde LL(1) significa:

L (Left-to-Right): Se lee la cadena de izquierda a derecha.


L (Left-most): Derivación por izquierda.
1: Símbolo de anticipación.

Construir un analizador sintáctico predictivo no recursivo por


medio de una pila es posible; el problema durante el análisis predictivo radica en
determinar la producción a aplicar a un no-terminal, partiendo del símbolo
analizado actualmente y el siguiente símbolo de entrada, para ello el analizador
sintáctico no recursivo busca en la tabla de análisis sintáctico que regla de
producción debe aplicar.

Pasos para reconocer una cadena por medio de LL(1):


• La cadena a reconocer, junto con el apuntador que indica el
token de pre-búsqueda actual, denotaremos por a al token

43
• Una pila de símbolos, el cual contendrá a los terminales y
no terminales, considérese X como la cima de la pila.
• Una tabla de chequeo de sintaxis, asociada de forma
univoca a la gramática, denotaremos por M a la tabla, el
cual tiene la configuración siguiente: M[N x T U {$}].

La cadena de entrada debe acabar con EOF (fin de cadena, en


adelante $), a fin de reconocer una cadena aplicando el presente algoritmo se debe
consultar de forma reiterada la tabla M hasta que se logre aceptar o rechazar la
sentencia, para el efecto se inicia con una pila que en su base contendrá a $ y el
axioma inicial S de la gramática sobre la misma. Los pasos a seguir durante la
consulta son excluyentes y se exponen a continuación:

1. Si X = a = $ entonces ACEPTAR.
2. Si X = a ≠ $ entonces
- Se desapila X, y
- Se avanza el apuntador.
3. Si X ∈ T y X ≠ a entonces RECHAZAR.
4. Si X ∈ N entonces consultar la entrada M[X, a] de la tabla, y:
- Sí M[X, a] está vacía: Rechazar.
- Sí M[X, a] no está vacía, se reemplaza X de la pila e
insertar el consecuente (parte derecha) en orden inverso.
Ejemplo; SI M[X, a] = {X → LMN}, quitar X de la
pila e insertar LMN en orden inverso, ó sea NML.

Una vez aplicada la regla, ya no se puede volver a desaplicar como


sucede con las gramáticas con retroceso.

La principal dificultad de este tipo de análisis radica en que existe


un número reducido de gramáticas LL(1), aunque si bien es cierto que existe un
algoritmo para convertir gramáticas de contexto libres a su equivalentes LL(1).

44
Operaciones para convertir una GLC en una Gramática
LL(1):
1. Limpiar la gramática, Eliminar símbolos muertos y símbolos
inaccesibles.
2. Factorizar por izquierda.
3. Eliminar recursividad por izquierda.

Gramática de partida G(E):


Ε → Ε + Ε|T
Τ→T∗F| F
P → L ÷ num
F → (E) | id
L →P-F
Q→T ^ F

Limpiando la gramática. Eliminando símbolos muertos:


Ε → Ε + Ε|T
Τ→T∗F| F
F → (E) | id
Q→T ^ F

Limpiando la gramática. Eliminando símbolos inaccesibles:


Ε → Ε + Ε|T
Τ→T∗F| F
F → (E) | id

Factorizado quedaría (Gramática equivalente):


Ε → T E'
E' → + TE' | ε
Τ → FT'
T' → ∗ FT' | ε
F → (E) | id

45
Construcción de la tabla de símbolos:
Para la construcción de la tabla de símbolos se utilizan las
funciones: PRIMERO (α).
SIGUIENTE (A).

PRIMERO (α): Si α es una cadena de símbolos gramaticales,


entonces, PRIMERO (α) es el conjunto de terminales que inician las cadenas

derivada de α. SI α ⇒ ∗ε entonces, ε ∈ PRIMERO (α )

Algoritmo para calcular PRIMERO(x):


- Si x es terminal entonces, PRIMERO(x) = {x}.
- Si x → ε es una producción añadir ε a PRIMERO(x).
- Si x → y1 , y 2 ,..., y k es una producción.
- Si a ∈ PRIMERO(Yi) y ε está en todos los PRIMERO(Yi)…
PRIMERO(Yi-1) entonces añadir a a PRIMERO(x).
- Si ε está en PRIMERO(Yj), para todo j = 1, 2, …, k entonces
añadir ε a PRIMERO(x).
- Ejecutar hasta que no se puedan añadir más elementos a
PRIMERO(x).

Algoritmo para calcular SIGUIENTE(A):


Aplicar a cada no-terminal de la gramática, hasta que no se puedan
añadir más elementos.

- Para el símbolo distinguido añádase $, a SIGUIENTE(A), si A


es axioma.
- Si hay una regla de producción Α → αΒβ , añádase al conjunto
PRIMERO( β ) a SIGUIENTE(B), excepto ε .
- Si Α → αΒ ó Α → αΒβ son reglas de producciones, tal que
PRIMERO( β ) contenga ε (β ⇒ *ε ) . Entonces añadir el
conjunto SIGUIENTE(A) a SIGUIENTE(B).

46
Figura 9 - PRIMEROS y SIGUIENTE.
Ejemplo: Sea una producción Ε → T E :
T E’

… …
PRIMEROS(T) PRIMEROS(E’) SIGUIENTES(T)
Fuente: Revelle 2006:10

Calculando PRIMEROS y SIGUIENTES, para gramática (E):


Primero(E): {(, id} Siguiente(E): { $, ) }
Primero(E’): {+, ε} Siguiente(E’): {$, )}
Primero(T): {(, id} Siguiente(T): {+, $}
Primero(T’): {*, ε} Siguiente(T’): {+, $, ) }
Primero(F): {(, id} Siguiente(F): {*, +, $, )}

Tabla 1 - Tabla de análisis sintáctico LL(1).


T
id + * ( ) $
N
E Ε → T E' Ε → T E'
E’ Ε' → + T E' E' → ε E' → ε

T T → FT' T → FT'
T’ T' → ε T → *FT' T' → ε T' → ε
F F → id F → (E)
Fuente: Gálvez et. al 2005:71

La tabla viene asociada exclusivamente a la gramática, y representa


su autómata finito con pila, este método es mucho más eficiente que el análisis
con retroceso e igual de eficiente que el análisis con funciones recursivas. Una
ventaja sobre el método con funciones recursivas es que se puede automatizar la
generación de las tablas, por ende una modificación en la gramática de entrada no

47
es fuente de grandes problemas desde el punto de vista de la construcción de una
nueva tabla.

Recuperación de errores:
Una cadena de entrada es errónea desde el punto de vista
sintáctico, sí se llega a una celda vacía en la tabla de símbolos, en este punto no
solo es útil detectar el error, si no buscar los mecanismos para tratarlos, para ello
el traductor debe:

1. Anunciar el error a fin de facilitar su localización.


2. Recuperarse y seguir analizando el resto de la cadena de
entrada.

Métodos de manejos de errores:


1. A nivel de frase (Ad-Hoc): Llenar todas las celdas vacías con
marcas que permitan seleccionar el ó los procedimientos más
adecuados para el tratamiento del error, para lo cual es
necesario estudiar las posibles causas en cada celda dada y las
acciones correctivas para el error.
2. Método Panic(Sincronización): Este método consiste en saltar
tantos símbolos de la secuencia de cadena de entrada que sean
necesarios, hasta llegar a un símbolo situado al tope de la pila
que permita la selección de una celda no vacía.
El proceso de recuperación por el método Panic, permite ser
optimizado para lo cual será necesario realizar una delimitación
de los conjuntos de símbolos de sincronización.

Delimitación de símbolos de sincronización: Consiste en


seleccionar los tipos de errores que se desean tratar y para cada caso en particular
seleccionar los símbolos de sincronización.

48
Casos más comunes:
1. Para cada símbolo no terminal A, incluir como símbolos de
sincronización los SIGUIENTES(A).
2. Si el lenguaje utiliza delimitadores de bloques o sentencias y se
omite el delimitador de fin, incluir como símbolos de
sincronización PRIMEROS del bloque o sentencia.
3. Si un no terminal A se deriva en una cadena vacía, resulta
imposible detectar el error.
4. Si un símbolo terminal ‘a’ aparece en el tope de la pila y no
parea con el símbolo de entrada, se puede optar por quitar el
símbolo terminal ‘a’ de la pila y emitir un mensaje indicando
que el mismo ha sido omitido.

Análisis ascendentes; este tipo de análisis tiene como finalidad la


construcción del árbol de análisis sintáctico, partiendo del token hasta llegar al
axioma inicial, disminuyendo la posibilidad de reglas mal aplicadas y ampliando
la gama de gramáticas con posibilidades de ser analizadas.

Esté o no presente retroceso como parte de la definición de la


gramática, la cadena de entrada estará divida en dos partes, α y β :

β : Secuencias de tokens por consumir ( β ∈ Τ ∗ ) y siempre


coincidirá con algún trozo de la parte derecha de la cadena de
entrada. Inicialmente β coincide con toda la cadena a reconocer,
incluido $.

α : Resto de la cadena de entrada, trozo de la cadena de entrada al


que se ha aplicado algunas de reglas de producción, en sentido
inverso ( α ∈ (Ν ∪ Τ) ), representando al árbol sintáctico visto

desde arriba, la cual se forma conforme se van aplicando las reglas

49
de producción a β , en otras palabras α es una forma sentencial
ascendente.

La mecánica de todo análisis ascendente consiste en ir aplicando


operaciones, partiendo de una configuración inicial (entiendase configuración por
el par α − β ), las cuales permiten transitar de una configuración de origen a una
de destino, finalizando una vez consumidos todos los tokens de entrada en β y la
configuración de destino sea en la que α represente al árbol sintáctico por
completo. Las operaciones disponibles son cuatro, las cuales se exponen a
continuación:

1. ACEPTAR: Se acepta la cadena β ≡ $ y α ≡ S.


2. RECHAZAR: Cuando la cadena de entrada no es válida.
3. Reducir: Consiste en aplicar una regla de producción hacía
atrás a algunos elementos situados a la derecha de α .
4. Desplazar: Consiste en quitar el terminal más a la izquierda de
β y ponerlo a la derecha de α .

Una cadena de entrada es aceptada cuando, α es el axioma inicial,


β es una tira nula y solo contiene $, en caso de que β no sea una tira nula, α no
sea el axioma inicial y no puede aplicarse ninguna regla, la cadena se rechaza.
Antes de llegar a aceptar o rechazar cadena alguna, primeramente se debe aplicar
todas las reducciones a α y luego realizar los desplazamientos.

Análisis ascendentes con retroceso; Este tipo de análisis al igual


que su par del descendente intenta probar todas operaciones posibles de reducir y
desplazar, por medio de la fuerza bruta, hasta llegar a construir el árbol sintáctico
o agotar todas las opciones, en cuyo caso rechaza la cadena de entrada.

50
Precondición: α ∈ (Ν ∪ Τ) y β ∈ Τ∗

AnálisisAscendenteConRetroceso (α , β )
Para cada pi ∈ Ρ hacer
Si consecuente(pi) ≡ cola( α )
α '− β ' = reducir α − β por la regla pi
Si α ≡ S and β ≡ ε
¡ Sentencia reconocida ! (ACEPTAR)
si no
AnálisisAscendenteConRetroceso (α ' , β ' )
Fin si
Fin si
Fin Para
Si β ∗ ε
α '− β ' = desplazar α − β
AnálisisAscendenteConRetroceso (α ' , β ' )
Fin si
Fin AnálisisAscendenteConRetroceso

Y la llamada al algoritmo:
AnálisisAscendenteConRetroceso( ε , CadenaAReconocer)
Si NOT ¡ Sentencia reconocida !
¡ Sentencia no reconocida ! (RECHAZAR)
Fin si

Ante gramáticas que posean reglas con épsilon (ε), el presente


algoritmo termina por producir recursividad infinita, debido a que dicha regla
puede aplicarse hacia atrás produciendo una rama infinita hacia arriba.

El método de análisis ascendente con retroceso consume más


rápidamente los tokens de entrada que el análisis descendente con retroceso, lo
que le permite tomar decisiones con mayor cantidad de información y de forma

51
más eficiente, aunque de igual modo resulta inviable para aplicaciones prácticas,
pues su ineficiencia frente a otros métodos sigue siendo notoria.

Análisis ascendente de gramáticas LR (k); Es el método más


difundido, conocida como gramática LR(k), permite el análisis de una amplia
gama gramáticas de libre contexto, en donde LR(k) significa:

L: Consume los símbolos de izquierda a derecha (en inglés, Left-


to-rigth).
R: Por reducir por la derecha (en inglés, Rightmost derivation).
k: Cantidad de símbolos anticipación que se utiliza para tomar las
decisiones, por defecto 1, razón por la cual es comúnmente conocida como LR(1).

El análisis LR resulta atractivo debido a diversas razones:


• Reconoce prácticamente todos los lenguajes derivados de
las gramáticas de contexto libres.
• Es el método de análisis sin retroceso más difundido, por su
fácil implementación.
• Realiza la detección de errores conforme se realiza el
análisis y realiza la recuperación de las mismas de forma
controlada.

Unas de las desventajas de este método radica en el hecho de que


construir manualmente un analizador sintáctico LR para una gramática de
contexto libre lleva mucho trabajo, afortunadamente existen las herramientas
generadoras, en las que proveyendo una gramática de contexto libre, se puede
generar el analizador sintáctico para la misma, si la gramática que se proporciona
al generador posee ambigüedades o construcciones difíciles de entender, emite un
mensaje al diseñador especificando la posible regla de producción, a fin de que el
mismo pueda verificarlo y corregirlo.

52
Los analizadores LR basan su funcionamiento en la existencia de
una tabla de análisis (acciones-transiciones) asociada a la gramática siendo en este
caso similar a los analizadores LL(1).

La confección de tabla de análisis podría resultar una tarea hasta si


se quiere algo compleja, si bien dependiendo del método seleccionado para hacer
la tabla la misma puede terminar siendo una tarea mecánica.

Existen tres métodos principales para la construcción de tablas


de análisis:
1. LR Sencillo (en inglés, SLR): El más fácil de aplicar aunque el
menos potente.
2. LR canónico ó LR(1): De los tres el más potente, y el más
complejo.
3. LALR (LR con examen de anticipación - en inglés, Look
Ahead LR): Es el método intermedio y el más difundido de las
tres, se implementa sin mucho esfuerzo de forma eficiente.

Un método es más potente que el otro en relación a la cantidad de


gramática admitidas, aunque a mayor número de gramáticas mayor complejidad.

La pila del autómata de pila utilizado para el análisis LR está


formado por parejas de símbolos (terminales y no terminales) y estados.

1. Si acción [Sm, ai] = Desplazar, el analizador inserta en la pila el


símbolo de entrada ai y el siguiente estado S, obtenido gracias a la evaluación
Goto [Sm, ai]. Ahora, ai+1 pasa a ser el símbolo de la entrada a leer y el analizador
se encuentra ahora en el estado S.
2. Si acción [Sm, ai] = Reducir, entonces el analizador ejecuta una
reducción utilizando la regla A β.
3. Si acción [Sm, ai] = Aceptar, el análisis se ha terminado con
éxito.

53
4. Si acción [Sm, ai] = Error, se ha descubierto un error. El analizar
llama al procedimiento de recuperación de error.

Figura 10 - Esquema Sinóptico de un analizador LR

Entrada a1 a2 a3 … an $

Pila
Sm
Xm Analizador LR
(Programa de proceso) Salida de
Sm-1 Análisis
Xm-1

S0
Tabla de Análisis
Estado Acción Goto

Fuente: Gálvez et. al 2005:78

Algoritmo para el Analizador LR:


Hacer que p apunte al primer símbolo de w$;
Mientras .t.
Sea S el estado situado en la parte superior de la pila;
Sea a el símbolo al que apunta p;
si acción[S, a] = Desplazar S’
Cargar a y luego S’ en la pila;
Hacer que p apunte al siguiente símbolo;
sino si acción[S, a] = Reducir A β
Sacar de la pila 2|β| símbolos pila;
Sea S’ el estado situado en el tope de la pila;
Cargar A y el resultado de goto[S’, A] en la pila;
Ejecutar las acciones semánticas de A β;
sino si acción [S, a] = Aceptar

54
retornar;
sino
error();
fin si
fin mientras

Ejemplo de aplicación del algoritmo: Evolución del análisis con


gramática LR para el reconocimiento de la siguiente secuencia de entrada id * id +
id:

Sea la gramática:
1. Ε → Ε + Ε
2. Ε → T
3. Τ → T ∗ F
4. Τ → F
5. F → (E)
6. F → id

Tabla 2 - Tabla de Análisis LR.

Estado Acción Goto


id + * ( ) $ E T F
0 D5 D4 1 2 3
1 D6 acp
2 R2 D7 R2 R2
3 R4 R4 R4 R4
4 D5 D4 8 2 3
5 R6 R6 R6 R6
6 D5 D4 9 3
7 D5 D4 1
Di: Desplazar hasta i.
8 D6 D1
9 R1 D7 R1 R1 Rj: Reducir por j.
10 R3 R3 R3 R3 acp: Aceptar.
11 R3 R3 R3 R3
Fuente: Aho et. al 1990:225

55
Tabla 3 - Reconocimiento de id*id+id.
Pila Entrada Acción-Goto
0 id * id + id$ Desplazar
0 id 5 * id + id$ Reducir: F -> id
0F3 * id + id$ Reducir: T -> F
0T2 * id + id$ Desplazar
0T2*7 id + id$ Desplazar
0 T 2 * 7 id 5 + id$ Reducir: F -> id
0 T 2 * 7 F 10 + id$ Reducir: T -> T * F
0T2 + id$ Reducir: E -> T
0E1 + id$ Desplazar
0E1+6 id$ Desplazar
0 E 1 + 6 id 5 $ Reducir: F -> id
0E1*6F3 $ Reducir: T -> F
0E1+6T9 $ Reducir: E -> E + T
0E1 $ Aceptar
Fuente: Aho et. al 1990:226

Construcción de tablas de análisis para un SLR (LR (0)):


Pivote (mango, asa): Es una subcadena que coincide con par parte
derecha de una producción (regla).

Prefijo viable: Secuencia de símbolos que se pueden formar de


izquierda a derecha hasta un símbolo t (terminal)

Ítems: Una producción representada entre paréntesis cuadrados “[”,


“]” y un punto “.” en la parte derecha de la producción.

El método SLR tiene como premisa construir un autómata finito de


determinista (en adelante, AFD) a partir de la gramática para reconocer los
prefijos viables. Los estados del AFD se obtienen gracias al agrupamiento de los
ítems asociados con cada producción de la gramática.

Construcción de una AFD para un analizador SLR:


1. Gramática aumentada: Sea S el símbolo inicial de una
gramática G, la cual se reescribe a fin de obtener una

56
gramática aumentada (G’) en la que S’ es el nuevo símbolo
inicial quedando S' → S .

2. Operación de Cierre o Clausura: Siendo I el conjunto de


ítems, la operación de cierre para I, denotaremos como
cierre(I), la cual será obtenida aplicando las siguientes
reglas.
a. I ∈ cierre(I).
b. Si [Α → α . Ββ] ∈ cierre(I) y existe la producción
Β → µ, entonces [ Β → .µ] ∈ cierre(I).
c. Repetir paso b hasta que no se añada nuevo ítem a
cierre(I).

Gramática(G): Gramática aumentada(G’):


1. Ε → Ε + T 1. Ε' → Ε
2. Ε → T 2. Ε → Ε + T
3. Τ → T ∗ F 3. Ε → T
4. Τ → F 4. Τ → T ∗ F
5. F → (E) 5. Τ → F
6. F → id 6. F → (E)
7. F → id

Sea I = { E' → ⋅ E }
Cierre(I)={[ E' → ⋅ E ], [ Ε → ⋅Ε + T ], [ Ε → ⋅T ], [ T → ⋅T * F ],

[ T → ⋅F ], [ F → ⋅(Ε) ], [ F → ⋅id ]}

3. Función GOTO: Función de transición de estados a partir


de uno dado y según el símbolo de entrada en cuestión.
Goto(I, X), donde I es el conjunto de ítems y X es un
símbolo de la gramática. Se define Goto(I, X), como el
cierre del conjunto de todos los ítems
[Α → αX.β] tal que [Α → α.Xβ]∈ I .

57
En base a la gramática anterior (G’) y dado el siguiente
conjunto de ítems:
I = { [Ε' → Ε ⋅], [Ε → Ε ⋅ +T ]}
Entonces:
goto[I, + ] = { [Ε → Ε ⋅ +T ]}

Función cierre( goto[I, + ] ):


Cierre( [Ε → Ε ⋅ +T ] ) = { [Ε → Ε + ⋅T ] , [T → ⋅T * F] , [T → ⋅F] ,

[F → ⋅(Ε)], [F → ⋅id ]}

Para obtener los estados asociados a una gramática SLR se debe


aplicar el siguiente algoritmo, de este modo se obtenemos el conjunto de ítems(C)
LR(0), en base a una gramática aumentada (G’):

Procedimiento ítems(G’):
C :={cierre({ [S' → ⋅ S] })};
Repetir
Para cada I ∈ C y para cada símbolo X ∈ (Ν ∪ Τ)
tal que goto(I, X) ∉ ε y ∉ C.
Añadir goto(I, X) a C
Fin Para
Hasta (que no se puedan añadir más ítems a C)
Retornar

Gramática aumentada (G’):


1. Ε' → Ε
2. Ε → Ε + T
3. Ε → T
4. Τ → T ∗ F
5. Τ → F
6. F → (E)
7. F → id

58
Figura 11 - Conjunto LR(0) canónico para E' → ⋅ E .
Colección canónica que define los estados del AFD para un SLR:
I0 = E' → ⋅ E I5 = goto(I0,id):
Ε → ⋅Ε + T F → ⋅id
Ε → ⋅T I6 = goto(I1,+):
T → ⋅T * F Ε → Ε + ⋅T
T → ⋅F T → ⋅T * F
F → ⋅(Ε) T → ⋅F
F → ⋅id F → ⋅(Ε)
F → ⋅id
I1 = goto(I0,E): I7 = goto(I2,*):
E' → E ⋅ T → T * ⋅F
Ε → Ε ⋅ +T F → ⋅(Ε)
F → ⋅id
I2 = goto(I0,T): I8 = goto(I4,E):
Ε → T⋅ F → (Ε ⋅)
T → T ⋅ *F Ε → Ε ⋅ +T
I3 = goto(I0,F): I9 = goto(I6,T):
T → F⋅ Ε → Ε +T⋅
T → T ⋅ *F
I4 = goto(I0,(): I10 = goto(I7,F):
F → (⋅Ε) T → T*F⋅
Ε → ⋅Ε + T
I11 = goto(I8,)):
Ε → ⋅T
F → (Ε ) ⋅
T → ⋅T * F
T → ⋅F
F → ⋅(Ε)
F → ⋅id
Fuente: Aho et. al 1990:231

59
Figura 12 - Diagrama AFD para los prefijos viables.

I0 E I1 + I6 T I9 * a I7
F a I3
( a I4
id a I5
T I2 * I7 F I1
( a0 I4
id a I5

F I3

( E ) I1
I4 I8
F + a1 I6
a I2
T
id a I3

id I5

Fuente: Aho et. al 1990:232

Algoritmo para la construcción de la tabla de análisis SLR:


Entrada: Gramática aumentada (G’).
Salida: Funciones acción y goto.
Método.

1. Construir C = {I0, I1,…, In}, colección de estados asociados


a la gramática aumentada (G’).

60
2. El estado i es construido partiendo de Ii, apartir de allí se
determinan las acciones del analizador sintáctico para el
estado i según los siguientes criterios:
a. Si [Α → α ⋅ aβ ] ∈ Ii y goto(Ii, a) = Ij, entonces
acción[i, a] = “desplazar j” , donde a ∈ símbolo
terminal.
b. Si [Α → α ⋅] ∈ Ii, acción[i, a] = “reducir Α → α ”.
Para a ∈ SIGUIENTE(A), donde A ≠ S’.
c. Si [S → S'⋅] ∈ Ii,, acción[i, $] = “aceptar”.
d. Si no coincide con ningún caso ó se generan reglas
contradictorias NO ES SLR.

3. Para todos los símbolos no terminales A construir


transiciones goto al estado i, siguiendo la regla:
Si goto(I, A)= Ij, entonces goto(I, A) = j.

4. Las entradas no reconocidas por las reglas 2 y 3, serán


consideradas “error”.

5. El estado inicial se construye a partir del conjunto de ítems


que contiene [S' → ⋅S] (cierre [S' → ⋅S] ).

Considerando la gramática aumentada (G’) del ejemplo anterior, se


considerará del conjunto de ítems de la serie canónica primeramente se toma I0.

I0:
E' → ⋅ E
Ε → ⋅Ε + T
Ε → ⋅T
T → ⋅T * F
T → ⋅F
F → ⋅(Ε)

61
F → ⋅id

Donde:
F → ⋅(Ε) , permite la entrada, acción[0, (] = desplazar 4.
F → ⋅id , permite la entrada, acción[0, id] = desplazar 5.

I1:
E' → E ⋅
Ε → Ε ⋅ +T

Donde:
E' → E ⋅ , permite la entrada, acción[1, $] = aceptar,
Ε → Ε ⋅ +T , permite la entrada, acción[1, +] = desplazar 6.

I2:
Ε → T⋅
T → T ⋅ *F

Donde:
Ε → T ⋅ , Siendo que SIGUIENTE(E)={$, +, (}, permite la entrada,
acción[2, $] = acción[2, +]= acción[2, )] = reducir
Ε → T.
T → T ⋅ *F , permite la entrada, acción[2, *] = desplazar 7.

Siguiendo paso a paso el algoritmo como se demostró en el


ejemplo, se llegará a construir la tabla de acción y goto.

Una gramática SLR(1), puede incurrir en error de desplazar/reducir


(shift/reduce), aún frente a gramáticas no ambiguas, por su parte las gramáticas
LR canónico y LALR proveen mayor tolerancia a este tipo de errores, aun
siempre existe la posibilidad de que alguna gramática no ambigua pueda producir

62
una tabla de acciones con conflictos desplazar/ reducir, más cabe resaltar que
dichas gramáticas son evitables en la construcción de lenguajes.

Construcción de tabla de análisis para un LR canónico ó


LR(1):
En los analizadores SLR: si [Α → α ⋅] ∈ Ii, acción[i, a] = “reducir
Α → α ”. Para a ∈ SIGUIENTE(A), donde A ≠ S’.

Sin embargo esto no siempre es posible debido a que


ocasionalmente el estado i podría estar en el tope de pila, quedando el prefijo
viable βα por debajo de dicho estado (i) y en consecuencia βA no puede ir seguida
de una a en una forma sentencial derecha, por lo que la reducción Α → α no sería
válida para la entrada a.

Para solucionar el problema expuesto anteriormente los


analizadores LR canónicos se valen de un símbolo de anticipación, de ésta forma
se pueden aplicar restricciones a reglas de reducciones no válidas.

Ítem del análisis sintáctico LR(1)4:


Sea [Α → α ⋅ β, a ] , donde Α → αβ es una producción y a es un
Terminal o el marcador derecho $.

Sea [Α → α ⋅] reducir por Α → α , si el siguiente símbolo de


entrada es a.

Un ítem LR(1) [Α → α ⋅ β, a ] es válido para un prefijo viable δα si:


S{
⇒ * δΑw {
⇒ δaβw, Donde:
pd pd

pd = Parte derecha.
w = Terminales.
a = PRIMERO(w) ó w es ε y a es $.

4
LR(1): El “1” significa la longitud del siguiente elemento, símbolo de anticipación.

63
El método de construcción de colección de ítems LR(1), funciona
básicamente de la misma manera que para LR(0), a excepción de la operación de
cierre y goto, considérese:

1. I0 = cierre ([S' → ⋅S ,$])

2. Si se tiene un ítem [Α → α ⋅ Bβ, a ] y la producción B → η ,

incluir cierre [Β → ⋅η, b] , donde b ∈ PRIMEROS (β α ) , por


tanto si δα es un prefijo viable valido de [Α → α ⋅ Bβ, a ]
implica que S{ ⇒ δaBβw, donde a ∈ PRIMEROS(w).
⇒ * δΑw {
pd pd

Si δaBβ ⇒ δaηβ y b ∈ PRIMEROS (β a ) , entonces δαη es


prefijo válido.

Construcción de la colección de ítems LR(1) para la gramática G’:


Entrada: Gramática aumentada (G’)
Salida: La colección de ítems LR(1) para (G’).

Procedimiento cierre(I)
Hacer
Para cada ítem [Α → α ⋅ Bβ, a ] ∈ I,
cada producción B → γ ∈ G’
y cada Terminal b en PRIMERO (β a )

tal que [Β → ⋅γ, b] ∉ I, añadir [Β → ⋅γ, b] a I


Fin Para
Mientras se puedan añadir más ítems a I
Retornar I
Retornar

64
Procedimiento goto(I, X)
Si J = colección de ítems [Α → αX ⋅ β, a ], tal que

[Α → αX ⋅ β, a ] ∈ I
retornar cierre(I)
Fin Si
Retornar

Procedimiento Ítems(G’)
C :={cierre({ [S' → ⋅ S,$] })}
Hacer
Para cada colección I ∈ C y
X tal que goto(I, X) ≠ φ y no esté en C
Añadir goto(I, X) a C
Fin Para
Mientras se puedan añadir colecciones de ítems a C
Retornar

Sea la siguiente gramática:


G(S): G’(S’):
S → CC S' → S$
C → cC | d S → CC
C → cC | d

Cálculo de primeros:
PRIMERO(S) = {c, d}
PRIMERO(C) = {c, d}
PRIMERO(C$) = {c, d}

Cálculo de colección de ítems cierre:


I0 = cierre ([S' → ⋅ S,$]) = { [S' → ⋅S ,$] , [S → ⋅CC,$] , [C → ⋅cC, c/d] ,

[C → ⋅d, c/d ] }

65
I1 = goto(I0, S) = cierre ([S' → S⋅,$]) = { [S' → S⋅,$] }

I2 = goto(I0, C) = cierre ([S → C ⋅ C,$]) = { [S → C ⋅ C,$] ,

[C → c ⋅ C,$ ] , [C → d⋅,$] }

I3 = goto(I0, c) = cierre ([C → c ⋅ C,$]) = { [C → c ⋅ C, c/d] ,

[C → ⋅cC, c/d] , [C → ⋅d, c/d ] }

I4 = goto(I0, d) = cierre ([C → d⋅, c/d ]) ={ [C → d⋅, c/d ] }

I5 = goto(I2, C) = cierre ([S → CC⋅,$]) ={ [S → CC⋅,$]}

I6 = goto(I2, c) = cierre ([C → c ⋅ C,$]) = { [C → c ⋅ C,$] ,

[C → ⋅cC,$], [C → ⋅d,$] }

I7 = goto(I2, d) = cierre ([C → d⋅,$]) ={ [C → d⋅,$] }

I8 = goto(I3,C) = cierre ([C → cC⋅, c/d ]) ={ [C → cC⋅, c/d ]}

I9 = goto(I3,c) = cierre ([C → c ⋅ C, c/d ]) = ¡ YA EXISTE !


I9 = goto(I3,d) = cierre ([C → d⋅, c/d ]) = ¡ YA EXISTE !
I9 = goto(I6,C) = cierre ([C → cC⋅,$]) ={ [C → cC⋅,$] }

I10 = goto(I6,c) = cierre ([C → c ⋅ C,$]) = ¡ YA EXISTE !


I10 = goto(I6,d) = cierre ([C → d⋅,$]) = ¡ YA EXISTE !

Ya no se puede seguir creando ítems y se termina.

66
Algoritmo para la construcción de la tabla de análisis LR canónico.
Entrada: Una gramática aumentada G’.
Salida: Funciones acción y goto de la tabla de análisis para LR
canónico.
Método:
1. Construir C={I1, I2, I3, …, In} la colección de ítems o estados
asociados a la gramática LR(1) para G’.
2. Partiendo de Ii, se construye el estado i. Las acciones a ser
aplicadas se determinan de la siguiente manera:
a. Si [Α → α ⋅ aβ , b] ∈ Ii y goto(I, a)= Ij,
acción[i, a] = “desplazar j”. a debe ser terminal.
b. Si [Α → α⋅, a ] ∈ Ii, A ≠ S’,
acción[i, a] = “reducir Α → α ”.

c. Si [S' → S⋅,$] ∈ Ii, acción[i, $] = “aceptar”.


Si se produce un conflicto, entonces la gramática no es
LR(1) y falla el algoritmo.
3. Construir goto(i, A) para todo no terminal, si goto(Ii, A) = Ii,
entonces goto[i, A] = j.
4. Las entradas en las tablas de análisis sin correspondencia con
las reglas 2 y 3 son etiquetadas como “error”.
5. A partir del ítem [S' → ⋅S ,$] , se construye el estado inicial del
analizador sintáctico.

67
Figura 13 - Grafo de transiciones para los goto de S' → S$ .

S' → ⋅S ,$
S → ⋅CC,$ S → C ⋅ C,$
C → ⋅cC, c/d C → c ⋅ C,$ S' → CC,$
C → ⋅d, c/d C → d⋅,$ I5
I0 I2
C → d ⋅ ,$
S' → S ⋅ ,$ I7
I1
c
c

C → C ⋅ C,$
C → c ⋅ C, c / d C → ⋅cC,$
C → ⋅cC, c/d C → d⋅,$
C → ⋅d, c/d I6
I3

C → d ⋅ , c/d C → cC ⋅ , c/d C → cC ⋅ ,$
I4 I8 I9

Fuente: Aho et. al 1990:241

Tabla 4 - Tabla de análisis LR(1) para S' → S$ .

Estado Acción Goto


c d $ S C
0 D3 D4 1 2
1 aceptar
2 D6 D7 5
3 D3 D4 8
4 R3 R3
5 R1
6 D6 D7 9
7 R3
8 R2 R2
9 R2
Fuente: Aho et. al 1990:242

68
Construcción de tabla de análisis LALR(1):
La construcción de tablas de análisis sintácticos por medio del
método LALR(1) es menos potente que por el método LR(1), pero más potente
que por el método SLR. Es el método de construcción de tablas de análisis más
utilizada por la facilidad de expresar las sintaxis de los lenguajes de programación
por medio de una gramática LALR, sumado a esto que las tablas de análisis
resultantes son considerablemente más pequeñas que las de LR(1).

Algoritmo para la construcción de la tabla de análisis LALR(1)


Entrada: Gramática Aumentada.
Salida: Acción y Goto.
Método:
1. Construir la colección de ítems C={I0, I1,…, In} y el AFD
correspondiente como en el método LR(1).
2. Se unen todos los estados que estén conformados por los
mismos ítems y solo difieran en el símbolo de anticipación,
esto dará como resultado un AFD simplificado.
3. C’ = {J0, J1,…, Jn} es la colección de ítems LR(1) resultante.
Construir a partir del estado Ji las acciones sintácticas para el
estado i. Si no aparece conflictos en la tabla resultante
LALR(1), la gramática es considerada LALR(1).
4. La construcción del goto se realiza de la siguiente manera; se
considera J como la unión de uno o más ítems LR(1), J= J1 U J2
U Jk3, entonces las producciones de goto(I1,X), goto(I2,X), …,
goto(In, X), son las mismas. Sea K la unión de todas las
colecciones que tienen la misma producción que goto(I1,X), lo
cual da como resultado que goto(J, X) = K.

Resultado de la unión de producciones LR(1):


I36:
I3=goto(I0,c)=cierre ([C → c ⋅ C,$]) ={ [C → c ⋅ C, c/d] , [C → ⋅cC, c/d]

, [C → ⋅d, c/d ] }

69
I6=goto(I2,c)=cierre ([C → c ⋅ C,$]) ={ [C → c ⋅ C,$] , [C → ⋅cC,$] ,

[C → ⋅d,$] }

I47:
I4=goto(I0,d)=cierre ([C → d⋅, c/d ]) ={ [C → d⋅, c/d ] }

I7=goto(I2,d)=cierre ([C → d⋅,$]) ={ [C → d⋅,$] }

I89:
I8=goto(I3,C)=cierre ([C → cC⋅, c/d ]) ={ [C → cC⋅, c/d]}
I9 = goto(I6,C) = cierre ([C → cC⋅,$]) ={ [C → cC⋅,$] }

Partiendo de la base del AFD para el método LR(1), construido


anteriormente se muestra un AFD simplificado para LALR(1).

Figura 14 - AFD Simplificado para LALR(1).


I0 S I1

C
d

I4, I7 d I2 C I5

c
d
c C
I3, I6 I8, I9

c
Fuente: Revelle 2006:27

70
Una construcción de tabla de símbolos LALR(1), no debería
arrojar conflictos de desplazar/reducir, puesto que se parte de la base de que la
gramática fue aceptada como LR(1), para ello primeramente la gramática no tuvo
que haber tenido conflictos de este tipo, ahora bien, pueden aparecer conflictos
reducir/reducir como resultado de la unión de estados del AFD, aún cuando la
gramática fuera LR(1).

Tabla 5 - Tabla de análisis LALR(1) para S' → S$ .

Estado Acción Goto


c d $ S C
0 D36 D47 1 2
1 aceptar
2 D36 D47 5
36 D36 D47 89
47 R3 R3 R3
5 R1
89 R2 R2 R2
Fuente: Aho et. al 1990:246

Ambigüedad y precedencia:
Toda gramática ambigua no es un LR, aunque existen situaciones
en las que las gramáticas ambiguas son valiosa utilidad en la programación de
lenguajes, como ser en la especificación de expresiones ú optimizaciones de
algunos casos especiales, Sin embargo, estando frente a construcciones con
gramáticas ambiguas se deben especificar construcciones para eliminar dichas
ambigüedades a fin de garantizar un solo árbol sintáctico para cada frase dada.

La precedencia y asociatividad operadores durante la construcción


de lenguajes nos aportan criterios, a fin de eliminar conflictos desplazar/reducir
que podrían presentarse debido a ambigüedades en la gramática.

La precedencia especifica el orden relativo de cada operador con


respecto a los demás operadores. El operador de más precedencia se evalúa antes
que el de menor precedencia.

71
Operación: A#B&C
(A#B)&C Si & < #
A#(B&C) Si # > &

La asociatividad de un operador binario define cómo se operan tres


o más operándoos con dicho operador. Existen dos tipos de asociatividad y las
mismas pueden ser por izquierda o por derecha, las cuales van ligadas al tipo de
derivación que se esté utilizando.

Operación: A#B#C#D.
Asociatividad por izquierda: ((A#B)#C)#D.
Asociatividad por derecha: A#(B#(C#D))

Desde el punto de vista de los lenguajes de programación:


Si op1 < op2, entonces en op2 debe aparecer desplazar y en op1
reducir.

Ambigüedad de los else-balanceados:


Esto produce un conflicto desplazar/reducir cuando se dispone de
una gramática una sentencia if-expr--then-sentencia-else-sentencia sin endif, para
lo cual se propone adoptar la asociatividad del else con el ultimo then, de este
modo se pasa el else a la pila y desplaza.

Recuperación de errores:
Los métodos de recuperación de errores en las gramáticas LR se
basan en:

Modo Panic: Descartar los símbolos de la entrada y/o sacar


símbolos de la pila hasta alcanzar una situación válida para continuar el análisis
(elimina el error y, probablemente, más información de entrada).

72
PROBLEMA: Decidir si se saca de la pila o se avanza en la
entrada.

Como solución sería posible la inserción de símbolos de


sincronización. Consiste en saltar símbolos de la entrada hasta alcanzar algún
símbolo de sincronización.

PROBLEMA: Si se omiten algunos de los símbolos de


sincronización se pueden perder demasiados símbolos de entrada.

SOLUCIÓN: Optimizar los símbolos de sincronización en base a


los símbolos seguidores e iniciales de cada símbolo no terminal de la gramática.

Tabla de análisis: Consiste en completar las celdas vacías de la


tabla de análisis con procedimientos para el tratamiento del error y recuperación
del mismo para continuar con el análisis.

Métodos complejos basados en el algoritmo de Graham-


Rhodes: Permite recuperarse ante un error y además, en algunos casos, reparar el
mismo.

El método distingue dos fases:


1. Fase de condensación: donde se acota el contexto del error.

2. Fase de corrección: donde se intenta corregir el error.

Basándose en el análisis de los símbolos próximos en el tope de la


pila y en la entrada, la fase de corrección decide si hay que sacar de la pila o saltar

73
II.1.3. Análisis Semántico
El análisis semántico se encarga de velar que lo que se está
intentando realizar tiene un significado coherente, el mismo se encarga de la
verificación de tipos de datos, compatibilidad de expresiones, ámbitos, flujos de
controles y coherencia en entre parámetros. El analizador semántico trabaja en
estrecha cooperación con el analizador sintáctico a fin de realizar su tarea y en
ocasiones se lo encuentra subordinado al mismo.

El traductor debe tener la suficiente capacidad de realizar


comprobaciones estáticas y dinámicas, las cuales se resumen en:

La comprobación estática se realiza durante la etapa de análisis (A.


Léxico, A. Sintáctico, A. Semántico) y se encarga de la:

- Comprobación de compatibilidad de tipos.


- Comprobación de flujos de control.
- Comprobación de unicidad.
- Comprobaciones relacionadas con el nombre.

En la traducción de una pasada, valiéndose de las gramáticas


atribuidas, desde el analizador sintáctico se realizan las llamadas a las rutinas
semánticas y dichas rutinas son las que finalmente llaman al generador de código,
en cambio en traductores de dos o más pasadas, el análisis semántico se encuentra
separado de la generación de código, comunicándose finalmente con este a través
de un archivo intermedio, cuyo contenido se resume en el árbol sintáctico.

Sea cual sea el número de pasadas las rutinas semánticas hacen uso
de los registros semánticos, el cual tiene una forma de pila (la pila semántica),
almacena información semántica asociada a los operándoos y en ocasiones
también de los operadores.

74
Tabla de símbolos:
La tabla de símbolos es una estructura de datos de alto rendimiento
la cual almacena toda información relevante sobre los identificadores que de
alguna u otra manera intervienen en el análisis de un programa de entrada.

Algunas funciones de las funciones que desempeñan la tabla de


símbolos son:
1. Realizar chequeos semánticos; Verificar que una frase tenga su
antecesor y sucesor correspondiente y en forma correcta.
2. Controlar el ámbito de una variable o sentencia.

Una tabla de símbolos es una estructura de dinámica, la cual


habitualmente permanece en memoria solo en tiempo de ejecución, simplifican
significativamente la tarea del análisis sintáctico, así como asistir al analizador
semántico y en la generación de código, de ahí lo crítico de contar con una tabla
de símbolos construida y manejada de forma eficiente, pues en gran parte de la
compilación interviene la tabla de símbolos.

Figura 15 - Interacción de etapas con la Tabla de Símbolos.


Análisis Léxico

Análisis Sintáctico

Tabla
Análisis Semántico
de
Símbolos

Generación de
Código

Optimización de
Código

Fuente: Elaboración propia, basado en Cueva 1998:26

75
El contenido de la tabla de símbolos varia según la necesidad que
el desarrollador del traductor tenga en el manejo de las informaciones relativas al
análisis, si bien, la información básica que debe aparecer en cualquier tabla de
símbolo está compuesta por:

1. Nombre del símbolo o elemento: Se refiere al nombre del


identificador, función u objeto a ser tratado. Si se limita el
tamaño del mismo se gana en agilidad al momento de crear en
la tabla y en la manipulación, a costas de la imposición de un
máximo en cuanto al nombre de los identificadores y de
desperdiciar espacios si el mismo fuera más corto que el
tamaño defino.

2. Atributos: Son necesarios para definir el símbolo a nivel de


análisis semántico y en la generación de códigos, estos pueden
variar de un lenguaje a otro debido a las propias características
de cada lenguaje y al diseño e implementación que el
desarrollador haya recurrido, en general los atributos pueden
ser:

a. Tipo: Esta necesidad es básica en todo lenguaje, debido


que a través de la misma se puede controlar el uso
coherente que se hace de ellas.

b. Dimensión ó tamaño: A fin de realizar un control más


eficiente, cuando se tiene la presencia de un arreglo se
recomienda almacenar su valor en forma explicita en la
tabla de símbolos.

c. Dirección de memoria: Toda instrucción que hace


referencia a una variable, bloque de código, función o
procedimiento precisa saber la ubicación del mismo en

76
tiempo de ejecución, a fin de generar el código
maquina.

d. Valor: Almacenar el valor en un interprete puede


resultar de mucho más sencillo si se almacena el valor
de un identificador, debido a que se compila y ejecuta al
mismo tiempo el programa, cuando se trata de
traductores es sin embargo es poco habitual almacenar
el valor.

e. Lista de información adicional: Número de línea en el


que se declaró un identificador, su ámbito, entre otros.
Estos varían de un lenguaje a otro y más bien a criterio
del desarrollador.

Construcción de la tabla de símbolos:


Durante la etapa del análisis léxico se deberá, crear la estructura
básica de la tabla de símbolos, insertar los símbolos detectados y almacenar en
que líneas del programa aparece, mientras que en la etapa de análisis sintáctico y
semántico se procede al rellenado de la estructura previamente iniciada durante el
análisis léxico, controlando que no hayan valores repetidos. Una tabla de símbolos
puede ser iniciada con cierta información que podría ser de utilidad, como ser las
constantes propias del lenguaje, librerías de funciones y/o las palabras reservadas,
lo cual puede llegar a simplificar en gran medida el trabajo del análisis
lexicográfico.

Las operaciones básicas aplicables a una tabla de símbolos son;


- La inserción,
- La consulta, y
- La modificación (agregar atributos nuevos).

77
Saber en que momento se pueden y se deben aplicar las
operaciones citadas, dependen exclusivamente del tipo del lenguaje:

En lenguajes con declaraciones de variables explicitas:

- Durante las declaraciones: Insertar.


- En una referencia: Consultar.

En lenguajes con declaraciones implícitas:

- Consultar: Si el identificador ya no se encuentra


incluida.
- Insertar: Si consultar resulta negativo.
- Ante un lenguaje con manejo de estructuras de bloques:
Crear sub-tablas para el manejo.

Implantar una tabla de símbolos:


Si bien es sabido que el acceso y la manipulación eficiente de la
tabla de símbolos son prioritarios en toda área concerniente a los traductores, se
puede arrancar construyendo una tabla sencilla hasta que las distintas etapas del
traductor cohesionen en forma perfecta entre sí, momento en que puede centrarse
esfuerzos en construir la tabla definitiva.

Tipos de comprobaciones:
• Estáticas: Las comprobaciones sintácticas y semánticas,
que se realizan en tiempo de ejecución.
• Dinámicas: Realizadas en tiempo de ejecución.
Las comprobaciones semánticas (estáticas) son de:

• Tipo: Verifica la compatibilidad de tipos de los operándoos


en las expresiones.

78
• Flujo de control: Verifica que las estructuras que transfieren
el flujo de control tenga a quien transferirlos y luego volver
a recibir el flujo de control devuelto.
• Unicidad: Verifica la presencia de símbolos de forma única.
Ejemplo: En Pascal cada variable debe ser única en todo el
programa.
• Relación de nombres: Un mismo nombre debe aparecer
más de una vez. Ejemplo, en ADA el nombre de un bloque
debe aparecer al principio y al final del bloque para que el
mismo sea válido.

Comprobación de tipos:
Es una de las tareas en las que el traductor invierte gran parte del
tiempo, pues de entre los errores los más comunes son los del tipo de dato.

Un tipo de dato sirve para que el programador indique con que fin
de va a llegar a utilizar tal o cual variable, de este modo el traductor puede
verificar si el programador ha cometido un error, asegurándose de que una
construcción sintáctica coincida con el previsto en su contexto.

Expresiones de tipos: Una expresión de tipo es una básicamente el


tipo de dato, la cual puede ser de tipo básico o formado por medio de la aplicación
de constructores de tipos a otras expresiones de tipos. Algunas expresiones de
tipos:

1. Tipos básicos (Tipos simples): boolean, char, integer, real,


entre otros.
2. Una expresión de tipos puede ser nombrada por un nombre, así
mismo un nombre de tipos es una expresión de tipos.
3. Un constructor de tipos aplicado a expresiones de tipos es una
expresión de tipos:

79
i. Matrices - Array (I, T): Es una expresión de tipo que se
representa con elementos de tipo T y conjunto de índices I.
En Pascal:
VAR A: array [1…10] of integer;
Le asocia la expresión tipo: array (1...10, integer) a
A
ii. Producto cartesiano: Sea T1 y T2 expresiones de tipo,
entonces T1 × T2 (producto cartesiano) es también una
expresión de tipo. Supone que x es asociativo por izquierda.
iii. Registros: Es como producto pero con nombre. Record
((dirección×integer)×(lexema×array(1..15,char)))
PASCAL Lenguaje C

type file=record typedef struct {


direccion : integer; int direccion;
lexema : array[1..15] of char ; char lexema[15] ;
end ; } file ;

iv. Apuntadores: Sea T una expresión de tipo, entonces


pointer(T) es una expresión de tipo. pointer(FILA) : apunta
a un objeto tipo FILA.
var p : ↑ fila .
v. Funciones: Matemáticamente las funciones transforman
elementos de un conjunto (dominio) a elementos de otros
conjuntos (rangos), llevando esto a los lenguajes de
programación se puede considerar una transformación de
un dominio D a un rango R, la expresión D → R indica el
tipo de dicha función.
Ejemplo:
char×char → pointer(integer)

function f(a, b : char) : ^integer;

80
4. Las expresiones de tipos pueden contener variables cuyos
valores son expresiones de tipos.

Una de las mejores alternativas de representación de las


expresiones de tipos son los grafos, pues por medio del método dirigido por
sintaxis se pueden construir grafos dirigidos acíclicos (GDA).

Figura 16 - Árbol y GDA para char x char → pointer(integer).



x pointer x pointer

char Integer
char char Integer

Fuente: Revelle 2007:43

Un sistema de tipos es un conjunto de reglas pre-establecidas para


asignar expresiones de tipos a las distintas partes de un programa, pudiendo
diferentes traductores ó procesadores del mismo lenguaje utilizar diferentes
sistemas de tipos.

Todo comprobador de tipos ha de disponer de:


• Asignación de tipos.
• Comprobador de tipos en las expresiones.
• Comprobador de tipos en las proposiciones o
sentencias.
• Comprobador de tipos de las funciones.

81
Básicamente se advierten dos tareas:
• De asignación.
• De evaluación y comprobación.

P→D;E;S
D → D ; D | id : T | ε
T → char | integer | array [ num ] of T | ↑ T
E → literal | num | id | E mod E | E [ E ] | E ↑ | E (E) | ε
S → id := E | if E then S | while E do S | S ; S

Las comprobaciones semánticas vendrán referidas a:


a. Asignación de tipos.
b. Comprobación de tipos en expresiones.
c. Comprobación de tipos en proposiciones.
d. Comprobación de tipos en funciones.

Asignación de tipos:
P→D;E;S
D→D;D

D → id : T {añadetipo(id.entrada,T.tipo);}
T → char {T.tipo := char;}
T → integer {T.tipo := integer ;}
T → array [ num ] of T1 {T.tipo := array(1...num.val,T1.tipo);}
T → ↑ T1 {T.tipo := pointer(T1.tipo);}

Comprobación de tipos en expresiones:


E → literal {E.tipo := char;}
E → num {E.tipo := integer;}
E → id {E.tipo := buscar(id.entrada);}
E → E1 mod E2 {E.tipo := if E1.tipo = integer and

82
E2.tipo = integer then integer
else error_tipo;}
E → E1 [ E2 ] {E.tipo := if E2.tipo = integer and
E1.tipo=array(s,t) then t
else error_tipo;}
E → E1 ↑ {E.tipo := if E1.tipo=pointer(t) then t
else error_tipo;}

Comprobación de tipos en proposiciones:


S → id := E {id.tipo := buscar(id.entrada);
S.tipo := if id.tipo = E.tipo
then vacio
else error_tipo;}
S → if E then S1 {S.tipo := if E.tipo = boolean
then S1.tipo
else error_tipo;}
S → while E do S1 {S.tipo := if E.tipo = boolean
then S1.tipo
else error_tipo;}
S → S1 ; S2 {S.tipo := if S1.tipo=vacio and S2.tipo=vacio
then vacio
else error_tipo;}

Comprobación de tipos en funciones: En la fase de declaraciones


ha de ser definido el tipo.
E → E1 (E2) {E.tipo := if E1.tipo = s and E2.tipo = s→t
then t
else error_tipo;}

Equivalencia de tipos:
Equivalencia estructural de las expresiones de tipos: Dos
expresiones de tipos son equivalentes estructuralmente si responde a un mismo

83
tipo básico o está formada como resultado de aplicar el mismo constructor de
tipos a expresiones equivalentes estructuralmente.

Se puede hallar equivalencias de dos expresiones de tipos


aplicando GDA ó por medio del algoritmo de análisis de los constructores.

Algoritmo de equivalencia estructural:


funciton equivest (s,t) : boolean;
begin
if s y t son el mismo tipo_básico then
return true ;
else if s = array(s2,s2) and t = array(t1,t2) then
return equivest (s1,t1) and equivest(s2,t2);
else if s = s1xs2 and t = t1xt2 then
return equivest(s1,t1);
else if s = s1 → s2 and t1 = t1 → t2 then
retuirn equivest(s1,t1) and equivest(s2,t2)
else
return false;
end;

Equivalencia de nombre: Cuando las expresiones de tipos son


nombradas, entonces dos expresiones de tipos son equivalentes de nombre si y
solo si ambas expresiones son idénticas.

Representación de las expresiones de tipos:


Mediante mapa de bit (usado por D.M. Ritchie para el compilador
de C). Se parte de los constructores básicos:

La principal ventaja es que se ahorra espacio:


Dos secuencias de bit distintas representan expresiones de tipos
distintos, pero con dos secuencias de bit iguales no podemos asegurar que se
correspondan con tipos equivalentes.

84
Figura17 - Representación de las expresiones de tipos.
Codificación Tipos básicos Codificación
pointer (T) : Expresión Puntero → 01 Boolean 0000
freturn (T) : Expresión función → 11 Char 0001
array (T) : Expresión array → 10 Integer 0010
Real 0011

Expresiones de Tipo Mapa de Bit


char 000000 0001
freturn(char) 000011 0001
pointer(freturn(char)) 000111 0001
array(pointer(freturn(char))) 100111 0001

Fuente: Revelle 2007:44

Conversión de tipos (coerción):


Implícita: Se realiza automáticamente por medio del traductor,
normalmente se aplica a situaciones donde la conversión no representa riesgo de
perdida de información, por ejemplo entero a real, pero no real a entero.

Explícita: El programador indica expresamente la conversión.


E → C_entera {E.tipo := integer;}
E → C_real {E.tipo := real;}
E → id {E.tipo := buscar(id.entrada)
E → E1 oper E2 {E.tipo := if E1.tipo = integer and E2.tipo =
integer then integer
else if E1.tipo = integer and E2.tipo =
real then real
else if E1.tipo = real and E2.tipo =
integer then real
else if E1.tipo = real and E2.tipo =
real then real
else error_tipo;}

85
Inconvenientes para el comprobador de tipos:
1. Sobrecarga de funciones y operadores: Un símbolo
sobrecargado será aquel cuyo significado estará directamente ligado con el
contexto, por ejemplo el operador ‘+’ es un símbolo sobrecargado en algunos
lenguajes, pues al mismo tiempo que realiza una suma de valores numéricos ante
presencia de valores de carácter el mismo actua como símbolo de concatenación.

El traductor antes de aplicar una operación debe evaluar


previamente el tipo de dato de los operándoos.

Ejemplo: En C.
a := b * c; // Producto.
a := * c; // Indirección.

2. Polimorfismo: Las proposiciones del cuerpo de una función


polimorfica pueden ejecutarse con argumentos de distintos tipos.

Ejemplo: Una función que calcula el tamaño de un array.


array → tamano_array(<id_array>)
donde: El id_array puede ser cualquier de tipo.

Una forma de solucionar esto es por medio del uso de las variables
de tipos, estas representan a expresiones de tipos, con lo cual se permite la
representación de expresión de tipos desconocidos.

Ejemplo:
Función desreferencia(p)
{
retornar *p;
}

86
Se desconoce el tipo de p, aunque si se puede precisar que es un
apuntador, por ende se podría representar β:=pointer(α), donde β y α son variables
de tipos, quedando finalmente la expresión de tipos para la función de la siguiente
manera.
pointer(α) → α

II.3 - ETAPA DE SINTESIS

Desde el punto de vista del traductor la etapa de síntesis se resume


en la traducción desde el lenguaje fuente al lenguaje objeto (en caso del presente
trabajo al lenguaje C), el cual es equivalente semánticamente al programa fuente,
partiendo de las estructuras generadas en las etapas de análisis, la etapa de síntesis
se encarga de proceder a la traducción de lenguajes.

La etapa de síntesis se encuentra estrechamente ligada a la maquina


destino, en este caso al tratarse de un traductor de lenguajes, podría decirse que la
maquina destino es el Lenguaje destino ó sea el Lenguaje C, cabe destacar que las
etapas de síntesis habitualmente estudiadas son las correspondientes a procesos de
traducción tipo compilador, los cuales difieren de lo que respecta a un traductor
fuente-fuente y en particular con el P2C, debido a que los traductores tipo
compiladores a fin de realizar la generación de código objeto realizan otras
operaciones innecesarias para una traducción fuente-fuente.

II.3.1 Generación de código


Desde el punto de vista de un traductor y en particular en lo que
respecta al presente trabajo, la generación de código está dado por el lenguaje
destino al cual debe ser traducido la entrada, el cual previamente debió de haber
superado las distintas etapas de análisis y llegando a esta instancia sin errores

87
léxicos, sintácticos y/o semánticos. Una mejor perspectiva de la posición que
ocupa el generador de códigos dentro del traductor ofrece la siguiente figura:

Figura 18 - Posición del generador de códigos en un traductor.

Programa Etapa de Generación Programa


fuente análisis de códigos Objeto

Tabla de
símbolos.

Fuente: Elaboración propia.

Tal y como se puede observar en la figura 18, la etapa de


generación de código viene posterior a las etapas de análisis y utiliza la tabla de
símbolos a fin de representar las propiedades de los objetos contenidos en la
misma, la cual fue cargada en las etapas de análisis.

II.4 – DEFINICIÓN DIRIGIDA POR SINTAXIS.


II.4.1. Definición dirigida por sintaxis por medio de gramáticas con atributos
Son especificaciones de alto nivel en donde el significado de una
frase se encuentra ligado directamente con la representación de su árbol de
análisis sintáctico, de acuerdo a su estructura de su sintáctica.

En la definición dirigida por sintaxis se procede a analizar


sintácticamente la cadena de componentes léxicos, se construye su árbol de
análisis y se realiza el recorrido por el mismo a fin de evaluar las reglas
semánticas asociadas a sus nodos, las cuales se dividen en dos subconjuntos
denominados, atributos sintetizados y atributos heredados, dichos atributos

88
pueden guardar información relacionada en la tabla de símbolos, generar código,
emitir errores y/o realizar otras tareas.

Formalmente en una definición dirigida por sintaxis una regla de


producción se representa Α → α , la cual se encuentra asociada a un conjunto de
reglas semánticas con la forma b:= f (c1, c2, …, ck) donde f es una función y/o:

1. b es un atributo sintetizado de A y c1, c2, …, ck son atributos


de símbolos de α , ó
2. b es un atributo heredado de uno de los símbolos de α , y c1,
c2, …, ck son atributos de los restantes símbolos de α , o de A.

Sea el caso que sea, el atributo de b depende de los atributos c1, c2,
…, ck.

Una gramática atribuida es una definición dirigida por sintaxis en


la que las funciones de las reglas semánticas no producen efectos secundarios.

Al definir las reglas semánticas también quedan establecidas las


dependencias entre los atributos las cuales son representadas por medio del grafo
de dependencia, cuya misión es la de establecer el orden de ejecución de las reglas
semánticas durante el análisis.

Atributos sintetizados:
Una definición dirigida por sintaxis, compuesta de forma exclusiva
por atributos sintetizados, es denominada definición con atributos sintetizados.
Los atributos sintetizados son de muy alta popularidad en la práctica.

El valor de un atributo sintetizado en un nodo es resultado del


cálculo de los valores de sus nodos hijos.

89
Atributos sintetizados:

Gramática G(L) Acciones Semánticas:


L → Ε '\n' imprimir(E.val)
Ε →Ε+T E.val := E.val + T.val
Ε →T E.val := T.val
Τ→T∗F T.val := T.val * F.val

Τ→ F T.val := F.val

F → (E) F.val := E.val


F.val:= NUM.val
F → NUM

Figura 19 - Árbol de análisis sintáctico para 4*5+6.

E.val=26 \n

E.val=20 + T.val=6

T.val=4 * F.val=5
F.val=6

E.val=4 Num.val=5
Num.val=3
Num.val=4

Fuente: Elaboración propia, basado en Aho et. al 1990:290.

Atributos heredados:
Un atributo heredado es aquel cuyo valor es hallado a partir del
cálculo de los valores de sus nodos hermanos y sus nodos padres. Calcular estos
atributos implica más bien, un análisis descendente de la sentencia.

90
Se puede implementar atributos heredados a fin de comprobar si un
identificador está situado en la parte izquierda o derecha en una asignación y de
esta manera decidir sobre si precisamos el valor del mismo o su dirección.

Atributos Heredados:

Acciones Semánticas:
D → TL L.her := T.tipo
T → entero T.tipo := entero
T → real T.tipo := real
L → L, ident L.her := L.her
Añadetipo(ident.ptr_tds,L.her)

L → ident Añadetipo(ident.ptr_tds,L.her)

Figura 20 - Árbol de análisis sintáctico con atributo heredado.


D

T.tipo := L.her L.her := real

,
real
L1.her := real
Id3

,
L2.her := real
Id2

Id1

Fuente: Aho et. al 1990:292

91
El anterior árbol de análisis sintáctico con atributo heredado es la
representación para la frase real id1, id2, id3, en cada nodo donde aparece el valor
L.her (tres en total) se determinan calculando el valor de T.tipo.

II.4.2. Grafos de dependencias


Un grafo de dependencia no es más que la representación gráfica
de las interdependencias entre los atributos de los nodos de un árbol de análisis
sintáctico.

Los pasos a seguir para construir un grafo de dependencia para un


determinado árbol de análisis sintáctico son:
para cada nodo n en el árbol de análisis sintáctico
para cada atributo a de la gramática en el nodo n
construir un nodo en el grafo de dependencia para a
para cada nodo n en el árbol de análisis sintáctico
para cada regla semántica de la forma b:= f(c1, c2, …, ck)
asociada con la producción en n
para i:= 1 hasta k
hacer un arco desde el nodo c1 al nodo b

92
Figura 21 - Grafo de dependencias.

L.her
T T.tipo L 3
2
1 4
real real , id3.ptr_tds
L L1.her Id3
5
, 6

L L2.her Id2 id2.ptr_tds

7
Id1 id1.ptr_tds

Fuente: Gálvez et. al 2005:99

Si un atributo b en un nodo del árbol sintáctico depende de un


atributo c, entonces se debe evaluar la regla semántica para b en su nodo, después
de la regla semántica que define a c.

Por cada nodo del árbol del análisis sintáctico el grafo de


dependencia posee un arco al nodo b desde el nodo c, si el atributo de b depende
del atributo de c. La dirección de los arcos determina el sentido de las
dependencias.

Orden de evaluación:
Es una secuencia m1, m2,…, mk de los nodos del grafo en el cual
los arcos se construyen de mi → m j , siendo este un arco desde mi a mj, o sea que

mi aparece antes que mj.

93
Una definición dirigida por sintaxis puede expresarse de la
siguiente manera:
2. Utiliza la gramática subyacente para construir un árbol
sintáctico para entrada.
3. La construcción del grafo de dependencia se realiza de la
forma descrita recientemente.
4. Por medio de la ordenación topológica, se establece la
secuencia de evaluación de las reglas semánticas, la cual
produce la traducción de la cadena de entrada y/o
desencadena su interpretación.

La etiqueta de los arcos (números), establece el orden topológico,


siendo para el ejemplo anterior el siguiente orden:
1. T.tipo := real
2. L.her := T.tipo
3. añadetipo(id3.ptr_tds, L.her)
4. L1.her := L.her
5. añadetipo(id2.ptr_tds, L.her)
6. L2.her := L1.her
7. añadetipo(id1.ptr_tds, L.her)

Si el grafo de dependencia generado para una gramática tiene un


ciclo, se dice que la gramática atribuida es circular, por ejemplo. Sea una regla:

M → NOP

con reglas semánticas:

M.m = f(N.n, O.o, P.p)


P.p = g(M.m)

94
Figura 22 - Grafo de dependencias circular.

M M.m

N N.n O O.o P P.p

Fuente: Elaboración propia, basado en Gálvez et. al 2005:101

Tipos de gramáticas atribuidas:


S-Atribuida: Es cuando todos sus atributos son sintetizados, la
estructura de la pila se adecua para que cada símbolo de la gramática disponga de
sus atributos asociados.

La evaluación de los atributos se realiza cuando se REDUCE.

L-Atribuida: Sea la producción A→X1 X2···Xn, la gramática es


L-Atribuida si todos los atributos heredados asociados con Xj, 1 ≤ j ≤ n, sólo
dependen de:

1. Los atributos asociados con los símbolos X1X2···Xj-1


2. Los atributos asociados con A

Toda gramática L-Atribuida puede ser evaluada mediante métodos


de análisis ascendente, la misma no produce ciclos y permiten orden de
evaluación en profundidad:

Algoritmo:
función visitaenprofundidad(nodo n){
Para cada hijo de m en n, de izquierda a derecha evaluar
los atributos heredados de m
Visitaenprofuncidad(m)
Fin Para

95
evaluar los atributos sintetizados de n
fin visitaenprofuncidad

II.4.3. Esquemas de traducción


Es como una definición dirigida por sintaxis, con la diferencia de
que el orden de evaluación de las reglas se muestran de forma explicita, las
acciones semánticas van encerradas entre llaves “{” y “}”, y se escribe en el lado
derecho de la producción:

M → nl{printf (n − l )}P

Cuando en un esquema de traducción se va a utilizar tanto atributos


heredados como atributos sintetizados, hay que guardar especial cuidado, por lo
que es saludable seguir las siguientes reglas a fin de garantizar que traductor hará
lo que se desea que haga:

1. Un atributo sintetizado del símbolo no terminal a la izquierda


de la producción sólo se puede evaluar cuando se han evaluado
los atributos de los que depende, por lo que se recomienda
situarlo al final de la producción, si bien, también puede
aparecer entre símbolos.
2. Una acción no debe referir un atributo sintetizado de un
símbolo que esté en la derecha de una acción.
3. Un atributo heredado de un símbolo del lado derecho de la
regla de producción se debe evaluar antes que el símbolo.

96
CAPITULO III - MARCO METODOLOGICO

III.1. Descripción de la profundidad y Diseño de la tesis


Para el desarrollo del presente trabajo de tesis se utilizó la técnica
de traducción dirigida por sintaxis, la cual consiste básicamente en que el
analizador sintáctico del lenguaje traductor es la encargada de manejar los
procesos que intervienen en la traducción, así bien se podría decir que el
analizador sintáctico es el director de orquesta del traductor.

El desarrollo de este traductor estuvo enfocado en proveer de las


siguientes construcciones al lenguaje.

• Declaración de identificadores:
o Simples.
o Estructuras (arreglos y punteros).
o Funciones.
• Expresiones:
o Aritméticas.
o Lógicas
o Funciones del Lenguaje:
Matemáticas.
Manipulación de cadenas.
Otras.
• Proposiciones:
o Asignación: Id simples, arreglos, punteros.
o Condiciones: Simples y compuestos.
o Estructuras de repetición: Condicionales y con contador.
o Entradas y salidas: Id simples, punteros y arreglos.
o Salidas para expresiones.
o Encabezados de funciones: Inicio y final del cuerpo de una
función.

97
III.2. Cómo se realizó la tesis
Inicialmente se procedió al estudio de cada componente del
lenguaje por separado, para lo cual hubo que realizar pruebas de escritorio que
permitieran entender la complejidad de cada una de las construcciones del
lenguaje tanto a ser traducido como del lenguaje destino, las pruebas de
escritorios consistieron en la escritura de programas en Pseudocodigo y el
correspondiente programa equivalente en Lenguaje C, dicho estudio permitió
tener certeza de la profundidad de análisis al que se debía someter cada
componente y/o aspecto del lenguaje a ser traducido, tanto en el diseño como en
la escritura de sus gramáticas.

Posterior a las pruebas realizadas, se procedió a escribir la


gramática, partiendo de una versión preliminar (tipo borrador), el cual sirvió de
base para la diferenciación de los distintos componentes léxicos que finalmente
formaron parte de la sintaxis del traductor. Así una vez obtenidos todos los
componente léxicos se procedió a la escritura del analizador léxico, a la vez que se
comenzaba con el proceso de escritura del analizador sintáctico, conforme se
avanzaba con la escritura del analizador léxico se iban realizando pruebas de
reconocimiento de cada uno de los componentes, así la realización casi en paralelo
del analizador léxico y sintáctico, permitió tener la certeza de que lo reconocido
por el analizador léxico era válido en el analizador sintáctico y lo requerido por
analizador sintáctico era realizador por analizador léxico.

Una vez finalizada la fase de desarrollo y pruebas en paralelo del


analizador léxico y sintáctico, se procedió al desarrollo del analizador semántico y
junto con esta, las distintas fases de comprobaciones semánticas, las cuales
anteceden a la etapa de traducción propiamente dicha. Todo el proceso de
traducción, desde el reconocimiento de los componentes léxicos, hasta la fase de
emisión de códigos en lenguaje C (lenguaje destino) fue realizada por medio la
técnica denominada traducción dirigida por la sintaxis.

98
III.3. Descripción de los instrumentos y procedimientos utilizados para la
recolección y tratamiento de la información.
Los datos necesarios para el desarrollo de la tesis fueron obtenidas
por medio de estudios del código fuente de programas en Pseudocódigos y
Lenguaje C, para lo cual se acudió a recopilaciones referente a dichos temas,
coleccionadas durante los años de estudios secundarios y universitarios.

Para el tratamiento de estos datos no ha sido utilizada ninguna


técnica especial de tratamiento de datos, más que la de separar los programas en
sus componentes más simples (en sentencias del lenguaje), a fin de poder
manejarlos con mayor facilidad y proceder al estudio de estas, tanto en sus
construcciones léxicas, sintácticas y semánticas en dependencia del contexto en la
que se la utilice.

III.4. Descripción de la muestra utilizada.


Desde el punto de vista del presente trabajo quizás el termino de
muestra como tal no sea aplicable, aunque haciendo una analogía podría decirse
que se ha utilizado las sentencias esenciales del lenguaje C, como son entrada y
salida estándar, declaración de identificadores, operaciones lógicas, operaciones
aritméticas, sentencias condicionales y sentencias cíclicas.

III.5. Adecuación de los métodos a los objetivos de la tesis.


Para la realización del presente trabajo de tesis, se han utilizado
métodos ó técnicas esenciales para el desarrollo de traductores, aunque si bien es
cierto está rama de la computación no tiene un orden especifico a seguir, existe un
patrón difundido al respecto, el cual contempla el diseño e implementación de una
analizador léxico, un analizador sintáctico, un analizador semántico y el traductor
propiamente dicho.

99
III.6. Tratamiento de los datos.
El proceso de desarrollo del traductor P2C comenzó con la
realización de una investigación sobre los traductores, partiendo con el estudio de
la teoría existente respecto a los lenguajes y autómatas y la construcción de
traductores. Se ha estudiado el metalenguaje de programación Yacc y el lenguaje
de programación C, herramientas utilizadas para el desarrollo.

100
CAPITULO IV - RESULTADOS

Como resultado del trabajo de tesis se ha obtenido un producto al


que se lo ha bautizado con el nombre de P2C (Pseudocodigo-a-Lenguaje C), el
mismo es un traductor de lenguajes, capaz de traducir programas escritos en
Pseudocódigos a Lenguaje C, estos programas antes de que fueran traducidos
pasan por una serie de análisis los cuales finalmente terminan generando un
código fuente en Lenguaje C, si en el programa Pseudocódigo no ha encontrado
error alguno, caso contrario este error se reporta al programador a fin de que lo
pueda corregir antes de continuar con la traducción. Al funcionamiento del
producto final, se lo probó con en base varios programas escritos en Pseudocodigo
y traducidos a Lenguaje C, estos últimos al ser compilados en el compilador del
Lenguaje C no produjeron error, por lo que se considera que la traducción es
optima.

A continuación se exponen los códigos fuentes resultantes de la


programación del “TRADUCTOR PSEUDOCODIGO A LENGUAJE C” (P2C),
según Objetivo General, incluyendo las particularidades y especificaciones
propuestas en los objetivos específicos.

IV.1. Código fuente de los Analizadores Sintáctico y Semántico del traductor


P2C.
Tal es así que, como se expone en el apartado II.2.2 Análisis
Sintáctico (Parser), el analizador sintáctico tiene como tarea agrupar los tokens
en forma jerárquica, construir las frases gramaticales y reconocerlas como parte
del lenguaje definido, en cuyo caso construye el árbol sintáctico, en tanto el
análisis semántico (ver apartado, II.2.3. Análisis Semántico), se encarga de velar
que lo que se está intentando realizar tiene un significado coherente.

%{
#include <stdio.h>

101
#include <iostream.h>
#include <stdlib.h>
#include <conio.h>
#include <string.h>
#include <math.h>

int lineno=1;
int idfun=0;
int auxf=0;
int indiParam=0;
int auxIndiParam=0;
%}
%union{

struct{
char nombre[30];
int TIPO;
char valor[500];
int constructor;
char dimension[5];
char dimensionc[5];
}completo;
struct{
int TIPO;
}TIPO;
struct{
char valor[80];
}valor;
struct{
char nombre[30];
}nombre;

102
}

%type <completo> expr cond e


%type <TIPO> tipo
%token LOG COMCAD SEN COS TAN EXP RES LOG10 POT CUA LON
ATACAD COPCAD
%token <completo> ID
%token <valor> NUM NUMREAL CADENA
%token VACIO CARACTER REAL ENTERO ELARGO
%token <nombre> OPERARIT OPERLOG
%token INICIO FIN SI SALIR RETORNAR
%token DE RETORNAR PARI PARD PARA DOSP ARROBA
%token ASIG COMA CORI CORD SINO SELECCIONAR CASO ROMPER
%token OTROSCASOS MIENTRAS HACER HASTA
%token LEER IMPRIMIR AST OR NOT AND
%left OPERLOG UMINUS
%nonassoc AST
%start prog

%% /* Comienza la sección de reglas */

prog: INICIO {
printf("//Comprobado para TC-Lite\n\n#include <stdio.h>\n#include
<conio.h>\n#include <string.h>\n#include <math.h>\n\nvoid
main(){\n\nclrscr();"); }
lsent
FIN {printf("\n\ngetch();\n} //Fin del cuerpo principal\n\n");} lfuncion
;
lsent : lsent sent
| sent
| lsent error {yyerror();}
| error {yyerror();}

103
;
sent : sent_entsal
| sent_declar
| sent_asig
| sent_ciclo
| sent_condi
| ID {
if(buscaTipo($1.nombre)==5) {
$1.TIPO=5;
if(buscaConstructor($1.nombre)!=2){
printf("\nError linea:%d - El IDentificador %s no es una
función",lineno,$1.nombre);
_exit(0);
}
auxf=buscaIdFuncion($1.nombre);
auxIndiParam=0;
}
}
PARI e PARD {
printf("\n%s(",$1.nombre);
printf("%s);",$4.valor);}
;
lfuncion : lfuncion funcion
|
;
funcion : tipo ID PARI {
if(buscar($2.nombre)==0){
printf("\nError linea:%d - Identificador %s no existe, debe declarar antes
de usarlo",lineno,$2.nombre);_exit(0);
}else {
if(buscaConstructor($2.nombre)==2){
$2.TIPO=buscaTipo($2.nombre);

104
switch($2.TIPO){
case 1: {printf("\nint %s(",$2.nombre); break;}
case 2: {printf("\nlong %s(",$2.nombre); break;}
case 3: {printf("\nfloat %s(",$2.nombre);break;}
case 4: {printf("\nchar %s(",$2.nombre); break;}
case 5: {printf("\nvoid %s(",$2.nombre); break;}
}
}else{
printf("\nError linea:%d - Identificador \'%s\' no es una
FUNCION",lineno,$2.nombre);_exit(0);}
}
}/*De la regla*/
list_declar PARD {printf("){");}
lsent RETORNAR PARI expr PARD
{
if($2.TIPO != 5) printf("\nreturn (%s);\n}",$11.valor);
else printf("\n}");
}
;
sent_entsal : LEER ID{
if(buscar($2.nombre)==0){
printf("\nError linea:%d - Identificador %s no existe, debe declarar antes
de usarlo",lineno,$2.nombre);_exit(0);
}else {
if(buscaConstructor($2.nombre)==1){
$2.TIPO=buscaTipo($2.nombre);
if($2.TIPO!=5){
switch($2.TIPO){
case 1: {printf("\nscanf(\"\%\%d\",&%s);",$2.nombre);break;}
case 2: {printf("\nscanf(\"\%\%d\",&%s);",$2.nombre);break;}
case 3: {printf("\nscanf(\"\%\%f\",&%s);",$2.nombre);break;}
case 4: {printf("\nscanf(\"\%\%s\",&%s);",$2.nombre);break;}

105
}

}else{
printf("\nError linea:%d - NO PUEDE INGRESAR VALOR A
UNA VARIABLE TIPO VACIA",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Identificador \'%s\' no es una
VARIABLE",lineno,$2.nombre);_exit(0);}
}
}/*De la regla*/
| LEER ID CORI expr CORD {
if(buscar($2.nombre)==0){
printf("\nError linea:%d - Identificador %s no existe, debe declarar antes
de usarlo",lineno,$2.nombre);_exit(0);
}else {
if(buscaConstructor($2.nombre)==4){
$2.TIPO=buscaTipo($2.nombre);
if($2.TIPO!=5){
if($4.TIPO < 3){
if(buscaDimensionF($2.nombre) >= atol($4.valor)){
switch($2.TIPO){
case 1:
{printf("\nscanf(\"\%\%d\",&%s[%s]);",$2.nombre,$4.valor);
break;}
case 2:
{printf("\nscanf(\"\%\%d\",&%s[%s]);",$2.nombre,$4.valor);
break;}
case 3:
{printf("\nscanf(\"\%\%f\",&%s[%s]);",$2.nombre,$4.valor);
break;}
case 4:
{printf("\nscanf(\"\%\%s\",&%s);",$2.nombre,$4.valor);

106
break;}
}
}else{
printf("\nError linea:%d - Indice fuera del rango
\'%s[%s]\'",lineno,$2.nombre,$4.valor);_exit(0);
}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser
entero",lineno);_exit(0);}

}else{
printf("\nError linea:%d - NO PUEDE INGRESAR VALOR A
UNA VARIABLE TIPO VACIA",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Identificador \'%s\' no es un
VECTOR",lineno,$2.nombre);_exit(0);}
}
}
| LEER ID CORI expr COMA expr CORD {
if(buscar($2.nombre)==0){
printf("\nError linea:%d - Identificador %s no existe, debe declarar antes
de usarlo",lineno,$2.nombre);_exit(0);
}else {
if(buscaConstructor($2.nombre)==5){
$2.TIPO=buscaTipo($2.nombre);
if($2.TIPO!=5){
if(($4.TIPO < 3) && ($6.TIPO < 3)){
if(buscaDimensionF($2.nombre) >= atol($4.valor)){
if(buscaDimensionC($2.nombre) >= atol($6.valor)){
switch($2.TIPO){
case 1:

107
{printf("\nscanf(\"\%\%d\",&%s[%s][%s]);",$2.nombre,$4.
valor,$6.valor);break;}
case 2:
{printf("\nscanf(\"\%\%d\",&%s[%s][%s]);",$2.nombre,$4.
valor,$6.valor);break;}
case 3:
{printf("\nscanf(\"\%\%f\",&%s[%s][%s]);",$2.nombre,$4.v
alor,$6.valor);break;}
}
}else{
printf("\nError linea:%d - Indice -COLUMNA- fuera del
rango \'%s[%s]\'",lineno,$2.nombre,$4.valor);_exit(0);
}
}else{
printf("\nError linea:%d - Indice -FILA- fuera del rango
\'%s[%s,\'",lineno,$2.nombre,$4.valor);_exit(0);
}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser
entero",lineno);_exit(0);}
}else{
printf("\nError linea:%d - NO PUEDE INGRESAR VALOR A UNA
VARIABLE TIPO VACIA",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Identificador \'%s\' no es una
MATRIZ",lineno,$2.nombre);_exit(0);}
}
}
| LEER AST ID {
if(buscar($3.nombre)==0){
printf("\nError linea:%d - Identificador %s no existe, debe declarar antes
de usarlo",lineno,$3.nombre);_exit(0);

108
}else {
if(buscaConstructor($3.nombre)==5){
$3.TIPO=buscaTipo($3.nombre);
if($3.TIPO!=5){
switch($3.TIPO){
case 1: {printf("\nscanf(\"\%\%d\",&%s);",$3.nombre);break;}
case 2: {printf("\nscanf(\"\%\%d\",&%s);",$3.nombre);break;}
case 3: {printf("\nscanf(\"\%\%f\",&%s);",$3.nombre);break;}
case 4: {printf("\nscanf(\"\%\%s\",%s);",$3.nombre);break;}
}

}else{
printf("\nError linea:%d - NO PUEDE INGRESAR VALOR A UNA
VARIABLE TIPO VACIA",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Identificador \'%s\' no es
PUNTERO",lineno,$3.nombre);_exit(0);}
}
}
| ARROBA expr COMA expr IMPRIMIR expr{
if((buscaTipo($2.nombre) > 2) && (buscaTipo($4.nombre) > 2) )
{
printf("\nError linea:%d - FILA, COLUMNA DEBE SER
ENTERO",lineno);_exit(0);
}else {
if($6.TIPO!=5){
switch($6.TIPO){
case 1:
{printf("\ngotoxy(%s,%s);\nprintf(\"\%\%d\",%s);",$4.valor,$2
.valor,$6.valor);break;}
case 2:

109
{printf("\ngotoxy(%s,%s);\nprintf(\"\%\%d\",%s);",$4.valor,$2
.valor,$6.valor);break;}
case 3:
{printf("\ngotoxy(%s,%s);\nprintf(\"\%\%f\",%s);",$4.valor,$2.
valor,$6.valor);break;}
/*CASE=4, Opción 1era.=Punteros, Opción 2da.= mensajes
comillados, Opción 3era.=Valores de cadena*/
case 4:
{if($6.constructor>0)
printf("\ngotoxy(%s,%s);\nprintf(\"\%\%s\",&%s);",$4.valo
r,$2.valor,$6.valor);
else {
if($6.constructor==0)
printf("\ngotoxy(%s,%s);\nprintf(%s);",$4.valor,$2.valor,$
6.valor);
else
printf("\ngotoxy(%s,%s);\nprintf(\"\%\%s\",%s);",$4.valor,
$2.valor,$6.valor);}
break;}
}

}else{printf("\nError linea:%d - NO PUEDE INGRESAR VALOR A


UNA VARIABLE TIPO VACIA",lineno);_exit(0);}
}
}
;
sent_declar : tipo ID{
if(buscar($2.nombre)==1){
printf("\nError linea:%d - Identificador %s ya existe, no puede ser
redefinido",lineno,$2.nombre);_exit(0);
}else {
$2.TIPO=$1.TIPO;

110
if($2.TIPO!=5){
insertar($2.nombre,$2.TIPO,1,"0","0",0);
switch($2.TIPO){
case 1: {printf("\nint %s;",$2.nombre);break;}
case 2: {printf("\nlong %s;",$2.nombre);break;}
case 3: {printf("\nfloat %s;",$2.nombre);break;}
case 4: {printf("\nchar %s;",$2.nombre);break;}
}

}else{
printf("\nError linea:%d - NO PUEDE DECLARAR VARIABLE
TIPO VACIO",lineno);_exit(0);}
}
} ldecl

| tipo ID CORI expr COMA expr CORD {


if(buscar($2.nombre)==1){
printf("\nError linea:%d - Identificador %s ya existe, no puede ser
redefinido",lineno,$2.nombre);_exit(0);
}else {
if($4.TIPO < 3 && $6.TIPO < 3){
if( (atol($4.valor) > 0 ) && (atol($6.valor) > 0) ){
$2.TIPO=$1.TIPO;
strcpy($2.dimension,$4.valor);
strcpy($2.dimensionc,$6.valor);

if($2.TIPO!=5){
insertar($2.nombre,$2.TIPO,5,$2.dimension,$2.dimensionc,0);
switch($2.TIPO){
case 1:
{printf("\nint
%s[%s][%s];",$2.nombre,$2.dimension,$2.dimensionc);break;}

111
case 2:
{printf("\nlong
%s[%s][%s];",$2.nombre,$2.dimension,$2.dimensionc);break;}
case 3:
{printf("\nfloat
%s[%s][%s];",$2.nombre,$2.dimension,$2.dimensionc);break;}
case 4:
{printf("\nchar
%s[%s][%s];",$2.nombre,$2.dimension,$2.dimensionc);break;}
}/*Del Switch*/
}else {
printf("\nError linea:%d - No puede declarar ARREGLO tipo
VACIO",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser mayor a
CERO (0)",lineno);_exit(0);}/*Del Else*/
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser
entero",lineno);_exit(0);}/*Del Else*/
}/*Else más Externo*/
} /*Acción de la Regla*/ ldecl
| tipo ID CORI expr CORD {
if(buscar($2.nombre)==1){
printf("\nError linea:%d - Identificador %s ya existe, no puede ser
redefinido",lineno,$2.nombre);_exit(0);
}else {
if($4.TIPO < 3 ){
if( atol($4.valor) > 0 ){
$2.TIPO=$1.TIPO;
strcpy($2.dimension,$4.valor);
if($2.TIPO!=5){
insertar($2.nombre,$2.TIPO,4,$2.dimension,"0",0);

112
switch($2.TIPO){
case 1: {printf("\nint %s[%s];",$2.nombre,$2.dimension);break;}
case 2: {printf("\nlong %s[%s];",$2.nombre,$2.dimension);break;}
case 3: {printf("\nfloat %s[%s];",$2.nombre,$2.dimension);break;}
case 4: {printf("\nchar %s[%s];",$2.nombre,$2.dimension);break;}
}/*Del Switch*/
}else {
printf("\nError linea:%d - No puede declarar ARREGLO tipo
VACIO",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser mayor a
CERO (0)",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser
entero",lineno);_exit(0);}
}/*Else más Externo*/
} /*Acción de la Regla*/ ldecl
| tipo AST ID{
if(buscar($3.nombre)==1){
printf("\nError linea:%d - Identificador %s ya existe, no puede ser
redefinido",lineno,$3.nombre);_exit(0);
_exit(0);
}else {
$3.TIPO=$1.TIPO;
if($3.TIPO!=5){
insertar($3.nombre,$3.TIPO,3,"0","0",0);
switch($3.TIPO){
case 1: {printf("\nint *%s;",$3.nombre);break;}
case 2: {printf("\nlong *%s;",$3.nombre);break;}
case 3: {printf("\nfloat *%s;",$3.nombre);break;}
case 4: {printf("\nchar *%s;",$3.nombre);break;}
}

113
}else{
printf("\nError linea:%d - NO PUEDE DECLARAR
APUNTADOR TIPO VACIO",lineno);_exit(0);}
}
} ldecl
| tipo ID PARI{
if(buscar($2.nombre)==1){
printf("\nError linea:%d - Identificador %s ya existe, no puede ser
redefinido",lineno,$2.nombre);_exit(0);
_exit(0);
}else {
$2.TIPO=$1.TIPO;
insertar($2.nombre,$2.TIPO,2,"0","0",idfun);
switch($2.TIPO){
case 1: {printf("\nint %s(",$2.nombre);break;}
case 2: {printf("\nlong %s(",$2.nombre);break;}
case 3: {printf("\nfloat %s(",$2.nombre);break;}
case 4: {printf("\nchar %s(",$2.nombre);break;}
case 5: {printf("\nvoid %s(",$2.nombre);break;}
}
}
indiParam=0;
} lparam {printf(");"); idfun++;}PARD ldecl
;
ldecl : COMA sent_declar
|
;
list_declar : tipo ID{
if(buscar($2.nombre)==1){
printf("\nError linea:%d - Identificador %s ya existe, no puede ser
redefinido",lineno,$2.nombre);_exit(0);

114
}else {
$2.TIPO=$1.TIPO;
if($2.TIPO!=5){
insertar($2.nombre,$2.TIPO,1,"0","0",0);
switch($2.TIPO){
case 1: {printf("int %s",$2.nombre);break;}
case 2: {printf("long %s",$2.nombre);break;}
case 3: {printf("float %s",$2.nombre);break;}
case 4: {printf("char %s",$2.nombre);break;}
}
}else{
printf("\nError linea:%d - NO PUEDE DECLARAR VARIABLE
TIPO VACIO",lineno);_exit(0);}
}
} lpdeclar

| tipo ID CORI expr COMA expr CORD {


if(buscar($2.nombre)==1){
printf("\nError linea:%d - IDENTIFICADOR YA, NO PUEDE SER
DUPLICADO",lineno);_exit(0);
}else {
if($4.TIPO < 3 && $6.TIPO < 3){
if( (atol($4.valor) > 0 ) && (atol($6.valor) > 0) ){
$2.TIPO=$1.TIPO;
strcpy($2.dimension,$4.valor);
strcpy($2.dimensionc,$6.valor);
if($2.TIPO!=5){
insertar($2.nombre,$2.TIPO,5,$2.dimension,$2.dimensionc,0);
switch($2.TIPO){
case 1:
{printf("int
%s[%s][%s]",$2.nombre,$2.dimension,$2.dimensionc);break;}

115
case 2:
{printf("long
%s[%s][%s]",$2.nombre,$2.dimension,$2.dimensionc);break;}
case 3:
{printf("float
%s[%s][%s]",$2.nombre,$2.dimension,$2.dimensionc);break;}
case 4:
{printf("char
%s[%s][%s]",$2.nombre,$2.dimension,$2.dimensionc);break;}
}/*Del Switch*/
}else {
printf("\nError linea:%d - No puede declarar ARREGLO tipo
VACIO",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser mayor a
CERO (0)",lineno);_exit(0);}/*Del Else*/
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser
entero",lineno);_exit(0);}/*Del Else*/
}/*Else más Externo*/
} /*Acción de la Regla*/ lpdeclar
| tipo ID CORI expr CORD {
if(buscar($2.nombre)==1){
printf("\nError linea:%d - Identificador %s ya existe, no puede ser
redefinido",lineno,$2.nombre);_exit(0);
}else {
if($4.TIPO < 3 ){
if( atol($4.valor) > 0 ){
$2.TIPO=$1.TIPO;
strcpy($2.dimension,$4.valor);

if($2.TIPO!=5){

116
insertar($2.nombre,$2.TIPO,4,$2.dimension,"0",0);

switch($2.TIPO){
case 1: {printf("int %s[%s]",$2.nombre,$2.dimension);break;}
case 2: {printf("long %s[%s]",$2.nombre,$2.dimension);break;}
case 3: {printf("float %s[%s]",$2.nombre,$2.dimension);break;}
case 4: {printf("char %s[%s]",$2.nombre,$2.dimension);break;}
}/*Del Switch*/
}else {
printf("\nError linea:%d - No puede declarar ARREGLO tipo
VACIO",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser mayor a
CERO (0)",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser
entero",lineno);_exit(0);}
}/*Else más Externo*/
} /*Acción de la Regla*/ lpdeclar
| tipo AST ID{
if(buscar($3.nombre)==1){
printf("\nError linea:%d - Identificador %s ya existe, no puede ser
redefinido",lineno,$3.nombre);_exit(0);
_exit(0);
}else {
$3.TIPO=$1.TIPO;
if($3.TIPO!=5){
insertar($3.nombre,$3.TIPO,3,"0","0",0);
switch($3.TIPO){
case 1: {printf("int *%s",$3.nombre);break;}
case 2: {printf("long *%s",$3.nombre);break;}
case 3: {printf("float *%s",$3.nombre);break;}

117
case 4: {printf("char *%s",$3.nombre);break;}
}
}else{
printf("\nError linea:%d - NO PUEDE DECLARAR
APUNTADOR TIPO VACIO",lineno);_exit(0);}
}
} lpdeclar
| tipo ID PARI{
if(buscar($2.nombre)==1){
printf("\nError linea:%d - Identificador %s ya existe, no puede ser
redefinido",lineno,$2.nombre);_exit(0);
_exit(0);
}else {
$2.TIPO=$1.TIPO;
insertar($2.nombre,$2.TIPO,2,"0","0",idfun);
switch($2.TIPO){
case 1: {printf("\nint %s(",$2.nombre);break;}
case 2: {printf("\nlong %s(",$2.nombre);break;}
case 3: {printf("\nfloat %s(",$2.nombre);break;}
case 4: {printf("\nchar %s(",$2.nombre);break;}
case 5: {printf("\nvoid %s(",$2.nombre);break;}
}
}
} lparam {printf(");"); idfun++;}PARD lpdeclar
;
lpdeclar : COMA {printf(",");} list_declar
|
;
lparam : tipo {insertarParam($1.TIPO,idfun,indiParam);
indiParam++;
switch($1.TIPO){
case 1: {printf("int");break;}

118
case 2: {printf("long");break;}
case 3: {printf("float");break;}
case 4: {printf("char");break;}
case 5: {printf("void");break;}
}
}lpar
;
lpar : COMA {printf(",");} lparam
|
;
tipo : VACIO {$$.TIPO=5;}
| ENTERO {$$.TIPO=1;}
| ELARGO {$$.TIPO=2;}
| REAL {$$.TIPO=3;}
| CARACTER {$$.TIPO=4;}
;
expr : NUM {if(atol($1.valor) <=32767){ /*entero <= 32767*/
$$.TIPO=1;
}else{
$$.TIPO=2;}
strcpy($$.valor,$1.valor);}
| NUMREAL {$$.TIPO=3;strcpy($$.valor,$1.valor);}
| ID {if(buscar($1.nombre)==1){
strcpy($$.nombre,$1.nombre);
strcpy($$.valor,$1.nombre);
$$.TIPO=buscaTipo($1.nombre);
$$.constructor=buscaConstructor($1.nombre);}
else{
printf("\nError linea:%d - IDentificador %s debe ser declarado
antes de ser utilizado",lineno,$1.nombre);_exit(0);}
}
| ID CORI expr CORD {if(buscar($1.nombre)==1){

119
if($3.TIPO<3){
if(atol($3.valor) <= buscaDimensionF($1.nombre)){
strcpy($$.nombre,$1.nombre);
strcpy($$.valor,$1.nombre);
strcat($$.valor,"[");
strcat($$.valor,$3.valor);
strcat($$.valor,"]");
$$.TIPO=buscaTipo($1.nombre);
$$.constructor=buscaConstructor($1.nombre);
}else{
printf("\nError linea:%d - Indice de ARREGLO fuera del
rango",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser
entero",lineno);_exit(0);}
}else{
printf("\nError linea:%d-IDentificador %s debe ser
declarado antes de ser
utilizado",lineno,$1.nombre);_exit(0);}
}
| ID CORI expr COMA expr CORD {if(buscar($1.nombre)==1){
if($3.TIPO< 3 && $5.TIPO < 3){
if( (atol($3.valor) <= buscaDimensionF($1.nombre) ) &&
(atol($5.valor) <= buscaDimensionC($1.nombre) ) ){
strcpy($$.nombre,$1.nombre);
strcpy($$.valor,$1.nombre);
strcat($$.valor,"[");
strcat($$.valor,$3.valor);
strcat($$.valor,",");
strcat($$.valor,$5.valor);
strcat($$.valor,"]");
$$.TIPO=buscaTipo($1.nombre);

120
$$.constructor=buscaConstructor($1.nombre);
}else{
printf("\nError linea:%d - Indice de ARREGLO fuera del
rango",lineno);_exit(0);}
}else{
printf("\nError linea:%d - Indice de ARREGLO debe ser
entero",lineno);_exit(0);}
}else{
printf("\nError linea:%d - IDentificador %s debe ser
declarado antes de ser
utilizado",lineno,$1.nombre);_exit(0);}
}
| CADENA {$$.TIPO=4; strcpy($$.valor,$1.valor);$$.constructor=0;}
| SEN PARI expr PARD {if($3.TIPO <= 3){
strcpy($$.valor,"sin");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,")");
$$.TIPO=3;
}
else{
printf("\nError linea:%d - VALOR DEBE SER
NÚMERICO",lineno);_exit(0);}
}
| COS PARI expr PARD {if($3.TIPO <= 3){
strcpy($$.valor,"cos");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,")");
$$.TIPO=3;
}
else{

121
printf("\nError linea:%d - VALOR DEBE SER
NÚMERICO",lineno);_exit(0);}
}
| TAN PARI expr PARD {if($3.TIPO <= 3){
strcpy($$.valor,"tan");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,")");
$$.TIPO=3;
}
else{
printf("\nError linea:%d - VALOR DEBE SER
NÚMERICO",lineno);_exit(0);}
}
| EXP PARI expr PARD {if($3.TIPO <= 3){
strcpy($$.valor,"exp");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,")");
$$.TIPO=3;
}
else{
printf("\nError linea:%d - VALOR DEBE SER
NÚMERICO",lineno);_exit(0);}
}
| LOG PARI expr PARD {if($3.TIPO <= 3){
strcpy($$.valor,"log");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,")");
$$.TIPO=3;
}

122
else{
printf("\nEError linea:%d - VALOR DEBE SER
NÚMERICO",lineno);_exit(0);}
}
| LOG10 PARI expr PARD {if($3.TIPO <= 3){
strcpy($$.valor,"log10");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,")");
$$.TIPO=3;
}
else{
printf("\nError linea:%d - VALOR DEBE SER
NÚMERICO",lineno);_exit(0);}
}
| POT PARI expr COMA expr PARD {if($3.TIPO <= 3 && $5.TIPO<=3){
strcpy($$.valor,"pow");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,",");
strcat($$.valor,$5.valor);
strcat($$.valor,")");
$$.TIPO=3;
}
else{
printf("\nError linea:%d - VALOR DEBE SER
NÚMERICO",lineno);_exit(0);}
}
| CUA PARI expr PARD {if($3.TIPO <= 3){
strcpy($$.valor,"sqrt");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);

123
strcat($$.valor,")");
$$.TIPO=3;}
else{
printf("\nError linea:%d - VALOR DEBE SER
NÚMERICO",lineno);_exit(0);}
}
| LON PARI expr PARD {if($3.TIPO == 4){
strcpy($$.valor,"strlen");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,")");
$$.TIPO=1;
}
else{
printf("\nError linea:%d - VALOR DE LA EXPRESION
DEBE SER UNA CADENA",lineno);_exit(0);}
}
| ATACAD PARI expr COMA expr PARD {
if($3.TIPO == 4 && $5.TIPO==4){
strcpy($$.valor,"strcat");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,",");
strcat($$.valor,$5.valor);
strcat($$.valor,")");
$$.TIPO=4;
$$.constructor=-1;
}
else{
printf("\nError linea:%d - VALOR DE LA EXPRESION
DEBE SER UNA CADENA",lineno);_exit(0);}
}

124
| COPCAD PARI expr COMA expr PARD {
if($3.TIPO == 4 && $5.TIPO==4){
if($3.constructor==0){
printf("\nError linea:%d - No puede almacenar datos en
una cadena de caracteres",lineno);_exit(0);}
else{
strcpy($$.valor,"strcpy");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,",");
strcat($$.valor,$5.valor);
strcat($$.valor,")");
$$.TIPO=4;
$$.constructor=-1;
}
}else{
printf("\nError linea:%d - VALOR DE LA EXPRESION
DEBE SER UNA CADENA",lineno);_exit(0);}
}
| COMCAD PARI expr COMA expr PARD {
if($3.TIPO == 4 && $5.TIPO==4){
strcpy($$.valor,"strcmp");
strcat($$.valor,"(");
strcat($$.valor,$3.valor);
strcat($$.valor,",");
strcat($$.valor,$5.valor);
strcat($$.valor,")");
$$.TIPO=1;
}
else{
printf("\nError linea:%d - VALOR DE LA EXPRESION
DEBE SER UNA CADENA",lineno);_exit(0);}

125
}
| PARI expr PARD {$$.TIPO=$2.TIPO;
strcpy($$.valor,"(");
strcat($$.valor,$2.valor);
strcat($$.valor,")");}
| AST ID {$$.TIPO=buscaTipo($2.nombre);
strcpy($$.valor,"*");
strcat($$.valor,$2.nombre);
$$.constructor=buscaConstructor($2.nombre);}
| expr OPERARIT expr {if($1.TIPO==$3.TIPO){
if($1.TIPO!=4){
strcpy($$.valor,$1.valor);
strcat($$.valor,$2.nombre);
strcat($$.valor,$3.valor);
$$.TIPO=$1.TIPO;}
else{
printf("\nError linea:%d - Para concatenar cadenas utilice la
sentencia COPCAD",lineno);_exit(0);
}
}else{
printf("\nError linea:%d - Tipos deben ser
iguales",lineno);_exit(0);
}
}
| expr AST expr { if($1.TIPO==$3.TIPO){
strcpy($$.valor,$1.valor);
strcat($$.valor,"*");
strcat($$.valor,$3.valor);}
else{
printf("\nError linea:%d - Tipos deben ser
iguales",lineno);_exit(0);}
}

126
| OPERARIT expr %prec UMINUS {
if(strcmp($1.nombre,"-")==0){
strcpy($$.valor,"-");
strcat($$.valor,$2.valor);
$$.TIPO=$2.TIPO;
}else{
printf("\nError linea:%d - Para negación UNARIA
utilice el signo \'-\'",lineno);_exit(0);
}
}

| ID {
$1.TIPO=buscaTipo($1.nombre);
if($1.TIPO==5){
printf("\nError linea:%d - Asignación incorrecta, función %s no
devuelve valor",lineno,$1.nombre);_exit(0);}
if(buscaConstructor($1.nombre)!=2){
printf("\nError linea:%d - El IDentificador no es una
función",lineno);_exit(0);
}
auxf=buscaIdFuncion($1.nombre);
auxIndiParam=0;
}
PARI e PARD {
$$.TIPO=$1.TIPO;
$$.constructor=2;
strcpy($$.valor,$1.nombre);
strcat($$.valor,"(");
strcat($$.valor,$4.valor);
strcat($$.valor,")");
}
;

127
e : expr {
if(comparaParametro($1.TIPO,auxf,auxIndiParam)==1){
switch($1.TIPO){
case 1: {
strcpy($$.valor,$1.valor);
break;}
case 2: {
strcpy($$.valor,$1.valor);
break;}
case 3: {
strcpy($$.valor,$1.valor);
break;}
case 4: {
strcpy($$.valor,$1.valor);
break;}
}
}else {
printf("\nError linea:%d - Parametros no
coinciden",lineno);_exit(0);
}
auxIndiParam++;
}
| e COMA expr {
if(comparaParametro($3.TIPO,auxf,auxIndiParam)==1){
switch($3.TIPO){
case 1: {
strcat($$.valor,",");
strcat($$.valor,$3.valor);
break; }
case 2: {
strcat($$.valor,",");
strcat($$.valor,$3.valor);

128
break; }
case 3: {
strcat($$.valor,",");
strcat($$.valor,$3.valor);
break; }
case 4: {
strcat($$.valor,",");
strcat($$.valor,$3.valor);
break; }
}
}else {
printf("\nError linea:%d - Parametros no
coinciden",lineno);_exit(0);
}

auxIndiParam++;
}
;
cond : expr OPERLOG expr {strcpy($$.valor,$1.valor);
strcat($$.valor,$2.nombre);
strcat($$.valor,$3.valor);
$$.TIPO=-2;}
| NOT expr { strcpy($$.valor,"!");
strcat($$.valor,$2.valor);
$$.TIPO=-2;}
| cond AND cond {
printf("%s\n",$$.valor);
strcpy($$.valor,$1.valor);
printf("%s\n",$$.valor);
strcat($$.valor," && ");
strcat($$.valor,$3.valor);
printf("%s\n",$$.valor);

129
}
| cond OR cond {
strcpy($$.valor,$1.valor);
strcat($$.valor," || ");
strcat($$.valor,$3.valor);
printf("%s\n",$$.valor);
}
;
sent_asig : ID ASIG expr{
if((buscaTipo($1.nombre)==1 || buscaTipo($1.nombre)==2 ) ||
(buscaTipo($1.nombre)==$3.TIPO)){
if($3.TIPO == 4){
switch(buscaConstructor($1.nombre)){
case 1: {
printf("\nError linea:%d - El ID debe ser un VECTOR
para contener cadena",lineno);break;}
case 2: {
printf("\nError linea:%d - una FUNCION no puede
recibir asignación de variable",lineno);break;}
case 3: {
printf("\nError linea:%d - Utilización anormal de puntero
a memoria",lineno);
printf("\nError linea:%d - El ID debe ser un VECTOR
para contener cadena",lineno); break;}
case 5: {
printf("\nError linea:%d - El ID debe ser un VECTOR
para contener cadena",lineno);break;}
default:{
if($3.constructor!=4){
if(buscaDimensionF($1.nombre) >= (strlen($3.valor)-2))
printf("\nstrcpy(%s,%s);",$1.nombre,$3.valor);
else

130
printf("\nError linea:%d - Cadena de mayor longitud
que dimensión del arreglo ",lineno);
}else{
if(buscaDimensionF($1.nombre) >=
buscaDimensionF($3.nombre))
printf("\nstrcpy(%s,%s);",$1.nombre,$3.valor);
else printf("\nError linea:%d - Arreglo origen de mayor
dimensión que destino",lineno);}
break;}
}

}
else
printf("\n%s=%s;",$1.nombre,$3.valor);
}else{
printf("\nError linea:%d - Tipos deben ser iguales",lineno);
_exit(0);}
}
| ID CORI expr CORD ASIG expr{
if(buscaTipo($1.nombre)==$6.TIPO){
if($6.TIPO == 4){
if(buscaDimensionF($1.nombre) >= (strlen($6.valor)-2))
printf("\nstrcpy(%s,%s);",$1.nombre,$6.valor);
else {
printf("\nError linea:%d - Cadena de mayor longitud
que dimensión del arreglo",lineno);
_exit(0);}
}else{
if(buscaDimensionF($1.nombre) >= atol($3.valor))
printf("\n%s[%s]=%s;",$1.nombre,$3.valor,$6.valor);
else{

131
printf("\nError linea:%d - Indice fuera del
rango",lineno); _exit(0);}
}
}else{
printf("\nError linea:%d - Tipos deben ser iguales",lineno);
_exit(0);}
}
| ID CORI expr COMA expr CORD ASIG expr{
if(buscaTipo($1.nombre)==$8.TIPO){
if($8.TIPO == 4){
if(buscaDimensionF($1.nombre) >= (strlen($8.valor)-2))
printf("\nstrcpy(%s,%s);",$1.nombre,$8.valor);
else {
printf("\nError linea:%d - Cadena de mayor longitud
que dimensión del arreglo ",lineno); _exit(0);}
}else{
if(buscaDimensionF($1.nombre) >= atol($3.valor) &&
buscaDimensionC($1.nombre) >= atol($5.valor))
printf("\n%s[%s][%s]=%s;",$1.nombre,$3.valor,$5.valor,
$8.valor);
else{
printf("\nError linea:%d - Indice fuera del
rango",lineno);_exit(0);}
}
}
else{
printf("\nError linea:%d - Tipos deben ser iguales",lineno);
_exit(0);}
}
| AST ID ASIG expr {
if(buscaTipo($2.nombre)==$4.TIPO){
if(buscaConstructor($2.nombre)==1){

132
printf("\n*%s=&%s",$2.nombre,$4.valor);}
else{
printf("\n*%s=%s;",$2.nombre,$4.valor);
}
}else{
printf("\nError linea:%d - Tipos deben ser iguales",lineno);
_exit(0);}
}
;
sent_ciclo : MIENTRAS cond {
printf("\nwhile(%s){",$2.valor);}
con_sent
FIN MIENTRAS {printf("\n}");}
|PARA cond HASTA expr COMA expr {
printf("\nfor(%s;%s",$2.valor,$2.nombre);
if(atol($6.valor) > 0 ) /* for(i=0;i< 10,1)*/
printf("<%s;%s+%s){",$4.valor,$2.nombre,$6.valor);
else{
if(atol($6.valor) < 0 ) /*for(i=0;i>10,1)*/
printf(">%s;%s-%s){",$4.valor,$2.nombre,$6.nombre);
else {
printf("\nError linea:%d - Incremento no puede ser CERO \'0\'",lineno);
_exit(0);
}
}
}
con_sent FIN PARA {printf("\n}");
}

;
sent_condi : SI cond {
if($2.TIPO==-2){

133
printf("\nif( %s ){",$2.valor);}
else{
printf("\nError linea:%d - Error de tipo",lineno);
_exit(0);
}
}
con_sent s FIN SI {printf("\n}");}

| SELECCIONAR expr {printf("\nswitch (%s){",$2.valor);} c FIN


SELECCIONAR {printf("\n}");}
;
s : SINO {printf("\n}else{");} con_sent {printf("}");}
|
;
c : CASO expr DOSP {printf("\ncase %s:{",$2.nombre);} lsent r c
|o
;
r : ROMPER {printf("\nbreak;}");}
|
;
o : OTROSCASOS DOSP {printf("\ndefault:{");} lsent {printf("}");}
|
;
con_sent : lsent aux_salir
| SALIR {printf("\nexit(0);");}
;
aux_salir : SALIR {printf("\nexit(0);");}
|
;

%% /* Comienza el programa */
#include "p2cl.c"

134
#include "tabla.c"
main()
{
int xb;
yyparse();
}
yyerror()
{
printf("Error de sintaxis, en la linea: %d\n", lineno);
}

IV.2. Tabla de símbolos en Lenguaje C del traductor P2C.

//Lista de Parametros.
struct param{
int Tparam;
int indice;
int idfun;
struct param *siguiente;
};
typedef struct param Parametro;
Parametro *primeroP=NULL, *ultimoP=NULL, *proximoP=NULL, *nuevop;

//Lista que permite insertar un nodo en cualquier lugar de la misma

struct nodo{
char nombre[30]; //Nombre de la variable, longitud 30
int tipo; //entero : 1, enterolargo: 2, real: 3, caracter:4, vacio:5
int constructor; //Variable: 1, Funcion:2, Puntero:3, Vector: 4, Matriz = 5
char dimension[5]; //Dimensión Vector ó Fila Matriz.

135
char dimensionc[5]; //Dimensión Columna Matriz.
int idfun; //Indice para conectar con parametros si es Función
struct nodo *siguiente;
};

typedef struct nodo tds;


tds *primero, *ultimo, *proximo, *nuevo;

//Declaraciones de funciones.
void insertar(char [], int, int, char [], char [],int);
int buscar(char []);
int buscaTipo(char []);
int buscaConstructor(char []);
int buscaDimensionF(char []);
int buscaDimensionC(char []);
int buscaIdFuncion(char []);
void insertarParam(int,int);
int comparaParametro(int,int);

//Funciones para la tabla.


void insertar(char identi[], int tip, int constru, char dimen[], char dimenc[],int
idfunci)
{
nuevo=(struct nodo *)malloc(sizeof(struct nodo));
strcpy((nuevo->nombre),identi);
nuevo->tipo=tip;
nuevo->constructor=constru;
nuevo->idfun=idfunci;
strcpy(nuevo->dimension,dimen);
strcpy(nuevo->dimensionc,dimenc);

136
if(primero==NULL){
nuevo->siguiente=NULL;
primero=nuevo;
ultimo=nuevo;
}
else{
nuevo->siguiente=NULL;
ultimo->siguiente=nuevo;
ultimo=nuevo;
}
}

//Busca valor.
int buscar(char identi[])
{
tds *auxiliar;
auxiliar=primero;
while(auxiliar!=NULL)
{
if (strcmp(auxiliar->nombre, identi)==0)
return 1;
else
auxiliar=auxiliar->siguiente;
}
return 0;
}

//Busca Tipo del Identificador.


int buscaTipo(char identi[])
{

137
tds *auxiliar;
auxiliar=primero;

while(auxiliar!=NULL)
{
if (strcmp(auxiliar->nombre, identi)==0)
return auxiliar->tipo;
else
auxiliar=auxiliar->siguiente;
}
return 0;
}

//Busca el Constructor del ID.


int buscaConstructor(char identi[])
{
tds *auxiliar;
auxiliar=primero;
while(auxiliar!=NULL)
{
if (strcmp(auxiliar->nombre, identi)==0)
return auxiliar->constructor;
else
auxiliar=auxiliar->siguiente;
}
getch();
}

// Busca Dimensión de Filas.


int buscaDimensionF(char identi[])

138
{
tds *auxiliar;
auxiliar=primero;
while(auxiliar!=NULL)
{
if (strcmp(auxiliar->nombre, identi)==0)
return atol(auxiliar->dimension);
else
auxiliar=auxiliar->siguiente;
}
return 0;
}

//Busca Dimensión de Columnas.


int buscaDimensionC(char identi[])
{
tds *auxiliar;
auxiliar=primero;
while(auxiliar!=NULL)
{
if (strcmp(auxiliar->nombre, identi)==0)
return atol(auxiliar->dimensionc);
else
auxiliar=auxiliar->siguiente;
}
return 0;
}

//Busca el ID de la función en la Lista principal.


int buscaIdFuncion(char identi[])
{

139
tds *auxiliar;
auxiliar=primero;
while(auxiliar!=NULL)
{
if (strcmp(auxiliar->nombre, identi)==0)
return auxiliar->idfun;
else
auxiliar=auxiliar->siguiente;
}
return 0;
}

//Inserta Parametros en la lista de parametros (Declaración de función)


void insertarParam(int parame, int idfunci,int indi){

nuevop=(struct param *)malloc(sizeof(struct param));


nuevop->Tparam=parame;
nuevop->idfun=idfunci;
nuevop->indice=indi;

if(primeroP==NULL){
nuevop->siguiente=NULL;
primeroP=nuevop;
ultimoP=nuevop;
}
else{
nuevop->siguiente=NULL;
ultimoP->siguiente=nuevop;
ultimoP=nuevop;
}
}

140
//Compara parametros con la lista enviada al invocar función.
int comparaParametro(int tParametro, int idfunci,int indi)
{
Parametro *ptrParam;
ptrParam=primeroP;
while(ptrParam!=NULL)
{
if((ptrParam->Tparam==tParametro)&&(ptrParam-
>idfun==idfunci)&&(ptrParam->indice==indi))
return 1;

ptrParam=ptrParam->siguiente;

}
return 0;
}

141
IV.3. Código fuente del Analizador Léxico del traductor P2C.
%{
#include <string.h>
extern lineno;
%}
E""
%%
{E}*\({E}* {return(PARI);}
{E}*\){E}* {return(PARD);}
{E}*\[{E}* {return(CORI);}
{E}*\]{E}* {return(CORD);}
({E}*"<"{E}*|{E}*">"{E}*|{E}*">="{E}*|{E}*"<="{E}*|{E}*"=="{E}*|{E}*
"="{E}*|{E}*"!="{E}*) {
strcpy(yylval.nombre.nombre,yytext);
return OPERLOG;
}
{E}*y{E}*|{E}*Y{E}* {return AND;}
{E}*o{E}*|{E}*O{E}* {return OR;}
{E}*no{E}*|{E}*NO{E}* {return NOT;}
{E}*(\+|\-|\/|^){E}* {
strcpy(yylval.nombre.nombre,yytext);
return OPERARIT;}
{E}*\:={E}* {return(ASIG);}
{E}*\,{E}* {return(COMA);}
{E}*\:{E}* {return(DOSP);}
{E}*\@{E}* {return(ARROBA);}
{E}*\*{E}* {return(AST);}
{E}*salir{E}*|{E}*SALIR{E}* {return SALIR;}
{E}*inicio{E}*|{E}*INICIO{E}* {return INICIO;}
{E}*fin{E}*|{E}*FIN{E}* {return(FIN);}
{E}*mientras{E}*|{E}*MIENTRAS{E}* {return(MIENTRAS);}
{E}*hacer{E}*|{E}*HACER{E}* {return(HACER );}
{E}*para{E}*|{E}*PARA{E}* {return(PARA);}
{E}*hasta{E}*|{E}*HASTA{E}* {return(HASTA);}
{E}*si{E}*|{E}*SI{E}* {return(SI);}
{E}*sino{E}*|{E}*SINO{E}* {return(SINO);}
{E}*seleccionar{E}*|{E}*SELECCIONAR{E}* {return(SELECCIONAR);}
{E}*caso{E}*|{E}*CASO{E}* {return(CASO);}
{E}*romper{E}*|{E}*ROMPER{E}* {return(ROMPER);}
{E}*otroscasos{E}*|{E}*OTROSCASOS{E}* {return(OTROSCASOS);}
{E}*retornar{E}*|{E}*RETORNAR{E}* {return(RETORNAR);}
{E}*entero{E}*|{E}*ENTERO{E}* {
yylval.TIPO.TIPO =1;
return(ENTERO);
}
{E}*elargo{E}*|{E}*ELARGO{E}* {
yylval.TIPO.TIPO =2;
return(ELARGO);

142
}
{E}*vacio{E}*|VACIO{E}* {yylval.TIPO.TIPO =5;
return(VACIO);
}
{E}*real{E}*|{E}*REAL{E}* {
yylval.TIPO.TIPO =3;
return(REAL);
}
{E}*caracter{E}*|{E}*CARACTER{E}* {
yylval.TIPO.TIPO =4;
return(CARACTER);
}
{E}*de{E}*|{E}*DE{E}* {return(DE);}
{E}*leer{E}*|{E}*LEER{E}* {return(LEER);}
{E}*imprimir{E}*|{E}*IMPRIMIR{E}* {return(IMPRIMIR);}
{E}*sen{E}*|{E}*SEN{E}* {yylval.TIPO.TIPO =3;return(SEN);}
{E}*cos{E}*|{E}*COS{E}* {yylval.TIPO.TIPO =3;return(COS);}
{E}*tan{E}*|{E}*TAN{E}* {yylval.TIPO.TIPO =3;return(TAN);}
{E}*exp{E}*|{E}*EXP{E}* {yylval.TIPO.TIPO =3;return(EXP);}
{E}*res{E}*|{E}*RES{E}* {yylval.TIPO.TIPO =3;return(RES);}
{E}*log{E}*|{E}*LOG{E}* {yylval.TIPO.TIPO =3;return(LOG);}
{E}*log10{E}*|{E}*LOG10{E}* {yylval.TIPO.TIPO =3;return(LOG10);}
{E}*pot{E}*|{E}*POT{E}* {yylval.TIPO.TIPO =3;return(POT);}
{E}*cua{E}*|{E}*CUA{E}* {yylval.TIPO.TIPO =3;return(CUA);}
{E}*lon{E}*|{E}*LON{E}* {yylval.TIPO.TIPO =1;return(LON);}
{E}*atacad{E}*|{E}*ATACAD{E}* {
yylval.TIPO.TIPO =4;
return(ATACAD);
}
{E}*copcad{E}*|{E}*COPCAD{E}* {
yylval.TIPO.TIPO =4;
return(COPCAD);
}
{E}*comcad{E}*|{E}*COMCAD{E}* {
yylval.TIPO.TIPO =1;
return(COMCAD);
}
{E}*[a-zA-Z][a-zA-Z0-9]* {
strcpy(yylval.completo.nombre,yytext);
return ID;
}
{E}*[0-9]+{E}* {
strcpy(yylval.valor.valor,yytext);
return NUM;
}
{E}*[0-9]+\.[0-9]+{E}* {
strcpy(yylval.valor.valor,yytext);
return NUMREAL;

143
}
\"[^\"]*\" {strcpy(yylval.valor.valor,yytext);
return CADENA;
}
\n {lineno++;}
. {return("simbolo no reconocido %s\n",yytext);}
%%

IV.4. Manual de programador del traductor P2C.


El presente Sub-Apartado de RESULTADOS corresponde al
manual de programador del traductor P2C, el cual posibilita al usuario
programador familiarizarse con las sentencias traductor P2C, posibilitando el uso
eficiente de las mismas.

1.- Estructura de una programa P2C:


INICIO
Cuerpo del programa principal
FIN
[Funciones de usuarios]

, donde:
INICIO | FIN: Delimitadores de inicio y fin del programa principal
Cuerpo del programa principal: Sentencias del programa principal.
Funciones de usuarios: Funciones definidas por el usuario
(opcional).

1.1.- Identificadores:
Todos los nombres utilizados para referenciar variables y
funciones, son referidos en forma genérica como identificadores.

144
1.1.1.- Identificadores definidos por el usuario:
Todos los identificadores definidos deben cumplir con las
siguientes reglas:
1. Pueden estar formados por:
a. Letras mayúsculas y/o minúsculas.
b. El carácter de subrayado.
c. Los dígitos del 0 al 9.
2. Longitud máxima de caracteres en el nombre: 30
3. Se toma en cuenta la diferencia entre letras mayúsculas y
minúsculas, por lo que: SUMA, Suma y suma son
identificadores diferentes.
4. No puede utilizarse el mismo identificador para dos
objetos.

1.2.- Palabras reservadas:


Las palabras reservadas, son identificadores con uso específicos y
el usuario no puede utilizarlo para almacenar datos.
leer tan pot
@f,c imprimir exp cua
sen log long
cos log10 atacad
copcad comcad si
sino para mientras
inicio otroscasos fin
caso entero elargo
vacio caracter romper
real salir retornar

145
1.3.- Entrada/ salida:
Las palabras reservadas de entrada/salida soportadas por el P2C
son:
Sentencia Leer:
Toma la entrada, normalmente del teclado y la almacena en
variables previamente declaradas.

Sintaxis:
leer <identificador>

Ejemplo:
entero valor
leer valor

Sentencia Imprimir:
Utilizada para desplegar información en pantalla.
Sintaxis:
@ <fil>, <col> imprimir <elemento>

Ejemplo1:
entero valor
@2,3 imprimir valor

Ejemplo 2:
entero valor
@2, (3-1) imprimir valor

2. Variables y operadores:
2.1.- Variables: Una variable es un espacio de memoria que tiene un nombre y un
tipo de dato asociado, el valor del mismo puede variar durante la ejecución,

146
cualquier variable a ser utilizada debe ser declarada previamente, caso contrario
producirá error.

Ejemplo1: Utilización de variables (uso incorrecto).


INICIO
LEER variable Se está utilizando antes de ser declarada.
ENTERO variable Se declara después de ser utilizado.
FIN

Ejemplo2: Utilización de variables (uso correcto).


INICIO
ENTERO variable Se declara la variable
LEER variable Se utiliza la variable.
FIN

2.2.- Operadores:
Todos los objetos y/o variables que manejados por P2C poseen un
tipo de dato asociado, el cual determina la cantidad de espacio de almacenamiento
a ser asignado a cada uno de ellos, de acuerdo al tipo con el cual fueron definidos,
así como el conjunto de operaciones posibles a aplicarse sobre los valores que
almacenan. Las operaciones son representadas a través de identificadores
denominados operadores.

2.2.1.- Operadores Aritméticos:


Se aplican sobre objetos y/o variables numéricas y son:
Sean: A:=10 y B:=20.

147
Tabla 6 – Operadores Aritméticos admitidos por el traductor P2C.
Operador Operación Ejemplo Resultado
+ Adición C:=A+B C:=30
- Sustracción C:=B-A C:=10
* Multiplicación C:=A*B C:=200
/ División C:=B/A C=2
^ Potenciación C:=A^2 C=100

2.2.2.- Operadores Relaciónales:


Utilizados para comparar dos valores (expresiones) a fin de
reducirlos, siendo los posibles valore, VERDADERO ó FALSO.

Tabla 7 – Operadores relacionales admitidos por el traductor P2C.


Operador Operación
> Mayor que
< Menor que
>= Mayor o igual que
<= Menor o igual que
== ó = Igual que
!= Diferente que o igual que

2.2.3.- Operadores Lógicos:


Aplicados sobre enunciados resultantes de las operaciones
relaciónales. Arrojan siempre un valor de verdad.

Tabla 8 - Operadores lógicos admitidos por el traductor P2C.


Operador Operación
y AND, Conjunción
o OR, Disyunción
no NOT, negación.

148
2.2.4.- Operador de Asignación:
Sirve para asignar valores a los objetos del lado izquierdo del
operador, con valores o resultados de alguna operación (lado derecho)

Tabla 9 – Operador de asignación admitido por el traductor P2C.


Operador Operación
:= Asignación

3.- Instrucciones de control:


3.1.- Instrucciones de secuencias
Son instrucciones seguidas formadas por una o varias expresiones
simples, una tras otra.
Instrucción_1
Instrucción_2
Instrucción_3

Instrucción_N

3.2.- Instrucciones de selección


Son instrucciones que permiten seleccionar una de varias
alternativas posibles.
3.2.1.- SI …[SINO…] FIN SI
Permite elegir una de entre dos opciones de ejecución:
Sintaxis:
SI (condición)
Sentencias_N
[SINO
Sentencias_N]
FIN SI

149
Al ejecutarse, primero se evalúa la condición y en caso de ser
afirmativa se ejecutará las instrucciones comprendidas dentro del bloque SI, caso
contrario las comprendidas dentro del SINO. La sentencia SINO es opcional, por
lo que si no se hallaré presente tampoco existirá el bloque de código
correspondiente a este.

Ejemplo1: SI… FIN SI


SI A >= 1
@2,2 IMPRIMIR “Hola mundo”
FIN SI.

Ejemplo2: SI… SINO… FIN SI


SI A >= 1
@2,2 IMPRIMIR “Hola mundo”
SINO
@2,2 IMPRIMIR “Adiós mundo cruel”
FIN SI

3.2.2.- SELECCIONAR
Permite elegir entre más de dos opciones de ejecución.

Sintaxis:
SELECCIONAR (expresión)
CASO constante1:
Sentencias [ROMPER
CASO constante2:
Sentencias ROMPER
OTROSCASOS:
Sentencias
FIN SELECCIONAR

150
Ejemplo:
INICIO
ENTERO valor
SELECCIONAR (valor)
CASO 1:
@2,2 IMPRIMIR “Valor es 1”
ROMPER
CASO 2:
@2,2 IMPRIMIR “Valor es 2”
ROMPER
OTROSCASOS:
@2,2 IMPRIMIR “Valor mayor a 2”
FIN SELECCIONAR
FIN

Al entrar en la instrucción SELECCIONAR, se evalúa la


expresión, si la misma coincide con el primer CASO se ejecutará el bloque de
código que contiene el mismo, hasta llegar a la instrucción ROMPER, la cual
literalmente romperá la ejecución, si la expresión coincide con el segundo CASO,
se ejecutará el bloque segundo de código hasta la instrucción ROMPER o hasta
que termine el mismo, si no coincide con ninguno se ejecutará el bloque de
instrucción de OTROSCASOS. En todos los casos al terminar la ejecución el
control se devuelve a la línea siguiente del FIN SELECCIONAR.

3.3.- Instrucciones de iteración


Son instrucciones que permiten la ejecución de un bloque de
código en forma repetida.

151
3.3.1.- MIENTRAS
Por medio de la sentencia de control MIENTRAS, se procede a
evaluar inicialmente la condición, si la misma arroja un valor verdadero se
procede a ejecutar las sentencias englobadas dentro del bloque, entre las cuales
debe existir un modificador de la condición, a fin de no quedar en un bucle
infinido.

Sintaxis:
MIENTRAS(condición)
Sentencia_N
FIN MIENTRAS

Ejemplo: MIENTRA
INICIO
ENTERO nro
nro = 2
MIENTRAS nro <= 12
@nro, 2 IMPRIMIR nro
nro := nro + 1
FIN MIENTRAS
FIN

3.3.2.- PARA
Una de las sentencias cíclicas de más fácil utilización y de gran
utilización.

Sintaxis:
PARA inicialización, condición, incremento(control)
sentencia_N
FIN PARA

152
Ejemplo: PARA
INICIO
ENTERO NRO
PARA NRO= =1 HASTA 50, 1
@NRO,2 IMPRIMIR NRO
FIN PARA
FIN

4.- Funciones
Todo lenguaje de programación debe proveer al usuario de la
posibilidad de crear funciones, P2C no es la excepción, para ello incluye unas
series de posibilidades.

Inicialmente se debe distinguir entre definir, declarar e invocar una


función, cuando se define una función se le está reservando un espacio de
almacenamiento de memoria, al declarar se está informando que sobre la
existencia de una función con ciertas características.

4.1.- Declaración de funciones


Se informa al compilador que más adelante se estará definiendo
una función con ciertas características, tipo de dato a retornar, nombre y
argumentos necesarios.

Ejemplo:
ENTERO suma(ENTERO, ENTERO)

Declaración de prototipo de funciones, en la cual se especifica que


la función lleva por nombre suma, retornará un valor entero, y como argumento
deberá recibir dos valores enteros.

153
Es necesario realizar tanto la declaración como la definición de una
función, puesto que si se realizará una invocación sin previamente declarar y/o
definir el compilador arrojará un error, pues desconocerá la existencia de la
función que se invoca.

4.2.- Definición de funciones


Definir una función implica reservarle un espacio de
almacenamiento de memoria, de acuerdo al tipo de datos a retornar.

Formato de una definición:


Tipo identificador_funcion (arg1, arg2,…, argN)
Sentencias_N
RETORNAR(expr)

Donde:
Tipo: Es el tipo de dato a RETORNAR al origen de la llamada.
Argumento: La cantidad de argumento es opcional.
Ejemplo: Función suma.
ENTERO suma(ENTERO val1, ENTERO val2)
ENTERO resu
resu:= val1 + val2
RETORNAR (res)

4.3.- Invocación de funciones


La invocación a una función es la llamada o transferencia del
control al mismo, así como sus parámetros necesarios para ejecutar sus sentencias.
Ejemplo: Invocación a la función suma
ENTERO resultado, parametro1, parametro2

154
resultado:= suma(parametro1, parametro2)
Se invoca una función suma, el cual devuelve un valor entero, se le
envía como parámetro dos valores enteros, siendo asignado el resultado a la
variable resultado, en este caso particular, tanto resultado, parametro1 y
parametro2 son de tipo entero.

4.4.- Paso de parámetros.


Al invocar una función, se suelen enviar parámetros, sobre los
cuales la función realiza sus tareas.

Existen dos métodos básicos de paso de parámetros, las cuales son


por referencia y por valor, en el caso puntual de P2C, utiliza paso de parámetros
por valor.

4.4.1.- Paso de parámetros por valor.


Un paso de parámetro por valor es cuando al ser invocada una
función se envía una copia de la variable a ella, por ende las variables con las que
la función estaría trabajando serán una copia del original y toda operación
aplicada sobre la misma no afectará a la variable de origen.

Del ejemplo anterior:


ENTERO resultado, parametro1, parametro2
resultado:= suma(parametro1, parametro2)

A la función suma se le envía dos valores como parámetros, tanto


parametro1 y parametro2, son enviados como parámetros por valor, esto significa
que la función suma antes de utilizar los valores debe declarar dos variables
enteras a las cuales deberá asignar los mismos.

155
4.4.2.- Recepción de parámetros:
ENTERO suma(ENTERO Valrecibido1, ENTERO Valrecibido 2)

Una vez hecho esto la función suma podrá trabajar con los valores
recepcionados sin inconvenientes y sin peligro de alterar el valor original.

5.- Arreglos
Son conjuntos de elementos del mismo tipo almacenados en forma de ubicaciones
de memoria, las mismas comparten el mismo nombre, pudiendo distinguirse un
elemento de otro a través de un subíndice.

5.1.- Declaración de arreglos.


Los elementos almacenados en los arreglos son variables de algún
tipo dado, los cuales al compartir un mismo nombre se pueden tratar como un solo
elemento. La sintaxis para la declaración de un arreglo es la siguiente:

tipo identificador[ <expresión_constante> ]

donde:
tipo: Es el tipo datos de los elementos que componen el arreglo.
Identificador: Es el nombre del arreglo.
expresión_constante: Es una expresión que al reducirse debe dar
como resultado un valor entero positivo, el cual será conocido como dimensión
del arreglo.

Ejemplo: Declación de arreglos:


caracter nombre[31] : Declara un arreglo unidimensional llamado
nombre compuesto de 31 elementos de tipo carácter.

156
entero vector[20] : Declara un arreglo unidimensional de nombre
vector, compuesto por 20 elementos de tipo entero.

entero matriz[20,10] : Declara un arreglo bidimensional de nombre


matriz, compuesto por 20 filas de elementos de tipo entero y 10 columnas de
elementos enteros respectivamente.

Todos los índices inician en cero hasta n-1, esto significa que en un
vector de 20 posiciones, la primera posición es el cero y la última es el 19 (o sea
n-1).

5.2.- Manejo de arreglos.


Lo que viene a continuación trata lo referente al acceso a los
elementos individuales de un arreglo, sea para asignarles valores o para utilizar
los valores almacenados.

5.2.1- Asignación de valores a arreglos.


Una vez declarado el arreglo, al mismo se debe asignar valores,
caso contrario este al ser traducido al Lenguaje C, terminará por contener
“basura”, debido a que este ultimo almacena elementos completamente
desconocidos por el programador en cada nueva declaración, hasta que se le
asigne valores.

La forma de asignar valor a un arreglo no es más que escribir el


nombre del arreglo con su índice adecuado, más el símbolo de asignación seguido
del valor a almacenar en el arreglo.

Ejemplo:
entero vector[5]
vector[1] := 10

157
Si se está en un caso de arreglos bidimensionales, se debe tener
especial cuidado en no olvidar que cada uno de los subíndices inicia en 0.

Ejemplo:
INICIO
entero matriz[20,10]
entero NRO
entero NRO2
PARA NRO= = 0 HASTA 19, 1
PARA NRO2= = 0 HASTA 9, 1
matriz[NRO,NRO2] := 1+NRO2
FIN PARA
FIN PARA
FIN

5.3.- Cadenas de caracteres.


El traductor P2C no tiene un tipo de dato dado para las cadenas de
caracteres, en su lugar permite definir un arreglo de tipo caracter que será tratado
como una cadena.

Ejemplo:
caracter cadena[19]

5.4.- Valores iniciales en cadenas de caracteres.


El traductor P2C permite asignación de valores a las cadenas, tal
cual se tratara un tipo de dato nativo. Sea un arreglo identificado como cadena, de
tipo caracter, con una dimensión de 19, la asignación de valores sería.

158
Ejemplo:
caracter cadena[19]
cadena:= “esto es una cadena”

En un arreglo de caracteres se permite utilizar hasta n-1 posiciones,


por ende, el arreglo cadena definida con una dimensión de 19, solo se puede
almacenar 18 valores.

5.5.- Funciones para el manejo de cadenas de caracteres.


El traductor P2C provee de algunas funciones elementales para el
manejo de cadenas, las cuales serán detalladas a continuación.

LON(cadena): Proporciona la longitud de la cadena que se le


envía como argumento.
ATACAD(cadena1, cadena2): Concatena dos cadenas y guarda el
resultado en la primera de ellas.
COPCAD(cadena1, cadena2): Realiza la copia de la cadena 1 a la
cadena 2.
COMCAD(cadena1, cadena2): Compara el contenido de la
cadena1 con el contenido de la cadena 2, devolviendo los siguientes valores:
Menor que cero (< 0): Si la cadena1 es de menor longitud que la
cadena2.
Igual a Cero (= 0): Si las cadenas son iguales.
Mayor que cero (> 0): Si la cadena1 es de mayor longitud que la
cadena2.

6.- Apuntadores
Los apuntadores son una de las herramientas más poderosas con la
que todo programador puede llegar a contar en cualquier lenguaje de

159
programación, unas de las utilidades que nos brinda es la de manejar datos de
longitud variable, este caso es el caso de por ejemplo una lista enlazada ó un
grafo.

Los apuntadores representan direcciones de memorias


correspondientes a otras variables.

6.1.- Declaración de apuntadores.


La declaración de un puntero, es la siguiente:
entero *apuntador

Donde:
apuntador: Es el nombre del apuntador.
*: Es operador de indirección (similar al símbolo de
multiplicación).

Para referirse al objeto apuntado, se antepone el operador de


indirección al nombre del apuntador.

P2C, únicamente proporciona soporte para las declaraciones de


punteros para variables y la utilización del operador de indirección.

7.- Programa de ejemplo utilización de P2C


A continuación se presenta un ejemplo de codificación y traductor
por medio de P2C.

7.1.- Programa fuente escrito en Pseudocódigo


inicio
caracter titulo[37]
entero l,entero d, caracter cadena[37],entero f

160
entero fun(entero,entero)

titulo := "MI PROGRAMA TRADUCIDO A LENGUAJE C"


@1,120/2 - lon(titulo) imprimir titulo
@3,2 imprimir "introduzca valor para l: "

leer l
cadena:="Este es el valor de l:"
@5,2 imprimir cadena
@5,lon(cadena)+2 imprimir l

d:= l * 2
f:= d+l
cadena:="Valor resultante de la suma de f+d:"
@7,2 imprimir cadena
@7,lon(cadena)+2 imprimir fun(d,f)

fin

entero fun(entero a,entero e)


entero suma
suma := a+e
retornar (suma)

7.2.- Procesos de traducción del programa P2C a Lenguaje C:


Una vez completada la escritura del programa se procede a
traducirlo, digitando el manda “p2c”, correspondiente a la llamada al traductor,
seguido del signo menor que “<”, nombre del programa p2c escrito, más el signo
mayor que “>”, seguido del nombre con el cual se desea almacenar el programa C
resultante.

161
Figura 23 - Momento en que se procede a traducir un programa P2C.

Si no se pone el nombre del archivo destino el resultado será


desplegado en pantalla, como en la siguiente figura (Figura V – 2).

Figura 24 - Salida de la traducción en lenguaje C por pantalla.

7.3.- Código destino resultante, en Lenguaje C:


//Comprobado para TC-Lite

#include <stdio.h>
#include <conio.h>

162
#include <string.h>
#include <math.h>

void main(){

clrscr();
char titulo[37];
strcpy(titulo,"MI PROGRAMA TRADUCIDO A LENGUAJE
C");
gotoxy(100/2 - strlen(titulo),1);
printf("%s",&titulo);
int l;
int d;
char cadena[37];
int f;
int fun(int,int);
gotoxy(2 ,3);
printf("introduzca valor para l: ");
scanf("%d",&l);
strcpy(cadena,"Este es el valor de l:");
gotoxy(2 ,4);
printf("%s",&cadena);
gotoxy(strlen(cadena)+2 ,4);
printf("%d",l);
d=l*2;
f=d+l;
strcpy(cadena,"Valor resultante de la suma de f+d:");
gotoxy(2 ,8);
printf("%s",&cadena);
gotoxy(strlen(cadena)+2 ,8);
printf("%d",fun(d,f));

163
getch();
} //Fin del cuerpo principal

int fun(int a,int e){


int suma;
suma=a+e;
return (suma);
}

7.4.- Pantallas de TC-Lite:

Figura 25 - Pantalla de Tc-Lite, previa a la compilación.

164
Figura 26 – Compilación del programa C generado.

Figura 27 - Compilación realizada sin errores

165
Figura 28 - Ejecución del programa compilado en C(parte 1).

Figura 29 - Ejecución del programa compilado en C (parte 2).

166
CAPITULO V – CONCLUSIONES

A continuación paso a detallar las conclusiones a las que he


llegado, comenzando por lo referente a:

Objetivo General
Desarrollar un traductor de códigos teniendo como origen el
Lenguaje Pseudocódigo y como destino el Lenguaje de C.

Se concluye que: El objetivo general del trabajo de tesis ha sido


satisfecho y superado fehacientemente, pues se logró crear un traductor de
lenguajes, cuyas traducciones están libre de errores.

Objetivo Específico 1:
Diseñar las construcciones sintácticas de un lenguaje Pseudo-
codificado, en español, de fácil utilización para el usuario.

Se concluye que: Se logró diseñar e implementar construcciones


sintácticas sencillas para el lenguaje Pseudocodigo creado.

Objetivo Específico 2:
Diseñar y desarrollar las analizadores léxicos y sintácticos para la
verificación de los programas fuentes.

Se concluye que: Se logró diseñar, programar e implementar los


analizadores léxicos y sintácticos, los cuales verifican y garantizan que todo el
programa fuente se encuentra libre de errores léxicos-sintácticos.

167
Objetivo Específico 3:
Diseñar y desarrollar un sistema de tipos de datos, para la
implementación de un comprobador de tipos, que permita la verificación del
programa fuente, en concordancia con la tipificación básica del lenguaje objeto.

Se concluye que: Se diseñó, desarrolló e implementó, un sistema


de tipos de datos para el traductor.

Objetivo Específico 4:
Diseñar y desarrollar la comprobación estática de unicidad y flujo
de control.

Se concluye que: Se logró diseñar, desarrollar e implementar un


comprobador de unicidad y de flujos de control para el traductor.

Objetivo Específico 5:
Diseñar y desarrollar el generador del código objeto.

Se concluye que: Se diseñó y desarrolló satisfactoriamente un


generador de código objetos, produciendo programas en Lenguaje C, libres de
errores.

Por lo expuesto anteriormente se considera haber cumplido a


cabalidad con todos los objetivos inicialmente trazados.

168
Igualmente el presente trabajo, he permitido llegar a demostrar
que, “los límites de la aplicación de la tecnología y el desarrollo de las ciencias,
radica en los límites del pensamiento humano”.

169
CAPITULO VI –RECOMENDACIONES

Se recomienda trabajar en la extensión de soporte a estructura de


tipos struct, manejo de archivos, ampliar las funciones matemáticas y soporte a
funciones de cadena. Así también como soporte para el Sistema Operativo Linux.

170
BIBLIOGRAFIA

ABRAXAS SOFTWARE “Compiler construction on personal computer with


pcyacc”, Abraxas Software Inc.
http://www.pcyacc.com
Aho, A.; Sethi, R. (1990), “Compiladores: Principios, técnicas y
herramientas”, Addison-Wesley Iberoamericana. Pags 225,226,231, 232, 241,
242, 246, 290, 292.
Araujo, A., (2004) “Compiladores - Estructura y Funcionamiento”.
http://mx.geocities.com/alfonsoaraujocardenas/index.html

Cueva Lovelle, J. M. (1998), “Cuaderno Nº 10, Concepto básicos de


procesadores de lenguajes”, Universidad de Oviedo, España. Pag 26

Díaz Fernández, E. (2003) “Transparencias - Procesadores de Lenguaje”,


Publicado por la Prof. Elena Díaz Fernández, España.

Gálvez Rojas, S.; Mora, M. (2005) “Java a tope: traductores y compiladores


con LEX/YACC, JFLEX/CUP y JAVACC”, Universidad de Malaga, España.
Pags. 12, 18, 24, 57, 71, 78, 81, 99, 101.
Gálvez Rojas, S. (2001) “Traductores, Compiladores e Interpretes”,
Universidad de Málaga, España. Pag. 10.
García, O. (2002) “Lenguajes, gramáticas y autómatas”, Universidad de
Burgos, España.
Garrido, A; Iñesta, J.; Moreno, F.; Pérez, J. (2002), “Diseño de compiladores”,
Universidad de Alicante, España.

Lesk, M.; Schmidt E., “Lex - A Lexical Analyzer Generator”.


http://dinosaur.compilertools.net/
Revelle, J. (2006:10) “Procesadores de Lenguajes, Tema 4. Métodos de
Análisis Sintáctico Descendente”. Pags. 10, 27.
Revelle, J. (2007:43) “Procesadores de Lenguajes, Tema 7. Análisis
Semántico”. Pags 43, 44.
Rodríguez Ávila, E. “Principia - Diseño de Compiladores”, M. en C. Eduardo
René Rodríguez Ávila.
http://homepage.mac.com/eravila/index.html

Stonebraker, M.; Wong, E.; Kreps, P.; Held, G. (1976) “The desing and
implementation of INGRES”, ACM Trans. Database Systems. Pags.189-202.

Suárez Ortega, J., (2006:1) “Apuntes de la materia Compiladores I”,


Publicado por el Prof. Lic. Julio Suárez Ortega, Universidad del Cono Sur de
las Américas, Paraguay. Pags 1-3.
http://es.geocities.com/jsuarezo2004/Materiales.htm

Vélez Reyes, J., (2003) “Procesadores de Lenguajes”, Publicado por Javier


Vélez Reyes (jvelez@lsi.uned.es), Universidad Nacional de Educación a
Distancia, España. Pag. 4.