Está en la página 1de 13

DATOS INFORMATIVOS

Facultad: Ingenierías Carrera: Tecnologías de la información

Asignatura: Programación IV Código: TICSI222755

Profesor: Ing. Darío Rodríguez, Mgtr. Periodo académico: 2S – 2024(febrero - junio)

Email: jaime.rodriguez.vizuete@utelvt.edu.ec

INTRODUCCIÓN
La asignatura de programación IV corresponde a la Unidad de Formación
Profesionalizante y al Campo de Formación de praxis profesional; Tiene como
propósito el estudio y comprensión del proceso de traducción (compilación) de un
código fuente a un código objeto (lenguaje máquina). La asignatura brindará al
estudiante la capacidad de desarrollar aplicaciones informáticas más eficientes
aplicando los conceptos relacionados con traductores y profundizando
progresivamente en el estudio del proceso de la compilación que comprende tres
etapas: Análisis Léxico (scanner) con el reconocimiento de tokens, Análisis Sintáctico
(Parsing) con la construcción de árboles sintácticos y el Análisis Semántico con
revisión de la estructura semántica.

OBJETIVO
Diferenciar las etapas de un compilador según la función que cumple el analizador
léxico, sintáctico y semántico, para traducir un código fuente a un lenguaje de
máquina, que sea comprendido por el computador.

UNIDAD 1. COMPILADORES

Lenguaje de programación
Los lenguajes de programación son notaciones que describen los cálculos a las
personas y las máquinas. Nuestra percepción del mundo en que vivimos depende de
los lenguajes de programación, ya que todo el software que se ejecuta en todas las
computadoras se escribió en algún lenguaje de programación. Pero antes de poder
ejecutar un programa, primero debe traducirse a un formato en el que una
computadora pueda ejecutarlo. Los sistemas de software que se encargan de esta
traducción se llaman compiladores.
Traductores

1
ELABORADO POR: Ing. Orlen Ismael Araujo Sandoval, Mg.
El traductor es una herramienta esencial en la programación o desarrollo,
encargándose de convertir código fuente de un determinado lenguaje de
programación a código máquina que puede «entender» directamente el ordenador.
De acuerdo al modo en que llevan a cabo el proceso de conversión, los traductores se
dividen en dos conjuntos: intérpretes y compiladores.
Compiladores
Dicho en forma simple, un compilador es un programa que puede leer un programa en
un lenguaje (el lenguaje fuente) y traducirlo en un programa equivalente en otro
lenguaje (el lenguaje destino). Una función importante del compilador es reportar
cualquier error en el programa fuente que detecte durante el proceso de traducción.

Intérprete
Un intérprete es otro tipo común de procesador de lenguaje. En vez de producir un
programa destino como una traducción, el intérprete nos da la apariencia de ejecutar
directamente las operaciones especificadas en el programa de origen (fuente) con las
entradas proporcionadas por el usuario.

Nota. BASIC, Perl, Python, Ruby y PHP son algunos de los lenguajes de programación
más famosos que dependen de un intérprete para ser traducidos de código fuente a
código máquina. Por ello, también suelen llamarse lenguajes interpretados.

La estructura de un compilador
Cuando se ha hablado de programas equivalentes en distintos lenguajes (fuente y
objeto), nos referimos a “tener el mismo significado” (aunque no fijemos ahora
exactamente el concepto significado). Por ello, la compilación requiere dos grandes
partes o tareas:
Análisis: en la que se analiza el programa fuente para dividirlo en componentes y
extraer de algún modo el significado
Síntesis: en la que el significado obtenido se escribe en el lenguaje objeto

Fases de un compilador

La evolución de los lenguajes de programación


Las primeras computadoras electrónicas aparecieron en la década de 1940 y se
programaban en lenguaje máquina, mediante secuencias de 0’s y 1’s que indicaban de
manera explícita a la computadora las operaciones que debía ejecutar, y en qué orden.
Las operaciones en sí eran de muy bajo nivel: mover datos de una ubicación a otra,
sumar el contenido de dos registros, comparar dos valores, etcétera. Está demás decir,
que este tipo de programación era lenta, tediosa y propensa a errores. Y una vez
escritos, los programas eran difíciles de comprender y modificar.

Tipos de lenguaje de programación


Hay tres tipos de lenguaje de programación:
 Lenguaje de máquina (lenguaje de bajo nivel).
 Lenguaje ensamblador (lenguaje de bajo nivel).
 Lenguaje de alto nivel.

El avance a los lenguajes de alto nivel


Un paso importante hacia los lenguajes de alto nivel se hizo en la segunda mitad de la
década de 1950, con el desarrollo de Fortran para la computación científica, Cobol
para el procesamiento de datos de negocios, y Lisp para la computación simbólica.
En la actualidad existen miles de lenguajes de programación. Pueden clasificarse en
una variedad de formas. Una de ellas es por generación. Los lenguajes de primera
generación son los lenguajes de máquina, los de segunda generación son los lenguajes
ensambladores, y los de tercera generación son los lenguajes de alto nivel, como
Fortran, Cobol, Lisp, C, C++, C# y Java. Los lenguajes de cuarta generación son
diseñados para aplicaciones específicas como NOMAD para la generación de reportes,
SQL para las consultas en bases de datos, y Postscript para el formato de texto. El
término lenguaje de quinta generación se aplica a los lenguajes basados en lógica y
restricciones, como Prolog y OPS5.
Un lenguaje de programación de alto nivel define una abstracción de programación: el
programador expresa un algoritmo usando el lenguaje, y el compilador debe traducir
el programa en el lenguaje de destino. Por lo general, es más fácil programar en los
lenguajes de programación de alto nivel, aunque son menos eficientes; es decir, los
programas destino se ejecutan con más lentitud. Los programadores que utilizan un
lenguaje de bajo nivel tienen más control sobre un cálculo y pueden, en principio,
producir código más eficiente. Por desgracia, los programas de menor nivel son más
difíciles de escribir y (peor aún) menos portables, más propensos a errores y más
difíciles de mantener. La optimización de los compiladores incluye técnicas para
mejorar el rendimiento del código generado, con las cuales se desplaza la ineficiencia
que dan las abstracciones de alto nivel.

TRABAJO
Ejercicio 1.1.1: ¿Cuál es la diferencia entre un compilador y un intérprete?
Ejercicio 1.1.2: ¿Cuáles son las ventajas de un compilador sobre un intérprete, y las de
un intérprete sobre un compilador?
Ejercicio 1.1.3: ¿Qué ventajas hay para un sistema de procesamiento de lenguajes en el
cual el compilador produce lenguaje ensamblador en vez de lenguaje máquina?
Ejercicio 1.1.4: Describa algunas de las tareas que necesita realizar un ensamblador.

Analizador Léxico
Como la primera fase de un compilador, la principal tarea del analizador léxico es leer
los caracteres de la entrada del programa fuente, agruparlos en lexemas y producir
como salida una secuencia de tokens para cada lexema en el programa fuente. El flujo
de tokens se envía al analizador sintáctico para su análisis. Con frecuencia el analizador
léxico interactúa también con la tabla de símbolos. Cuando el analizador léxico
descubre un lexema que constituye a un identificador, debe introducir ese lexema en
la tabla de símbolos. En algunos casos, el analizador léxico puede leer la información
relacionada con el tipo de información de la tabla de símbolos, como ayuda para
determinar el token apropiado que debe pasar al analizador sintáctico.
Por lo regular, la interacción se implementa haciendo que el analizador sintáctico llame
al analizador léxico. La llamada, sugerida por el comando obtenerSiguienteToken, hace
que el analizador léxico lea los caracteres de su entrada hasta que pueda identificar el
siguiente lexema y producirlo para el siguiente token, el cual devuelve al analizador
sintáctico.

Una de esas tareas es eliminar los comentarios y el espacio en blanco (caracteres de


espacio, nueva línea, tabulador y tal vez otros caracteres que se utilicen para separar
tokens en la entrada). Otra de las tareas es correlacionar los mensajes de error
generados por el compilador con el programa fuente.

Algunas veces, los analizadores léxicos se dividen en una cascada de dos procesos:
a) El escaneo consiste en los procesos simples que no requieren la determinación de
tokens de la entrada, como la eliminación de comentarios y la compactación de los
caracteres de espacio en blanco consecutivos en uno solo.
b) El propio análisis léxico es la porción más compleja, en donde el escanear produce la
secuencia de tokens como salida.

En muchos lenguajes de programación, las siguientes clases cubren la mayoría, si no es


que todos, los tokens:

Tokens, patrones y lexemas


Al hablar sobre el análisis léxico, utilizamos tres términos distintos, pero relacionados:
 Un token es un par que consiste en un nombre de token y un valor de atributo
opcional.
El nombre del token es un símbolo abstracto que representa un tipo de unidad
léxica; por ejemplo, una palabra clave específica o una secuencia de caracteres
de entrada que denotan un identificador. Los nombres de los tokens son los
símbolos de entrada que procesa el analizador sintáctico. A partir de este
momento, en general escribiremos el nombre de un token en negrita. Con
frecuencia nos referiremos a un token por su nombre.
 Un patrón es una descripción de la forma que pueden tomar los lexemas de un
token. En el caso de una palabra clave como token, el patrón es sólo la
secuencia de caracteres que forman la palabra clave. Para los identificadores y
algunos otros tokens, el patrón es una estructura más compleja que se
relaciona mediante muchas cadenas.
 Un lexema es una secuencia de caracteres en el programa fuente, que
coinciden con el patrón para un token y que el analizador léxico identifica como
una instancia de ese token.

1. Un token para cada palabra clave. El patrón para una palabra clave es el mismo
que para la palabra clave en sí.
2. Los tokens para los operadores, ya sea en forma individual o en clases como el
token comparación, mencionado en la figura anterior.
3. Un token que representa a todos los identificadores.
4. Uno o más tokens que representan a las constantes, como los números y las
cadenas de literales.
5. Tokens para cada signo de puntuación, como los paréntesis izquierdo y
derecho, la coma y el signo de punto y coma.

Análisis Sintáctico
La segunda fase del compilador es el análisis sintáctico o parsing. El parser (analizador
sintáctico) utiliza los primeros componentes de los tokens producidos por el analizador
de léxico para crear una representación intermedia en forma de árbol que describa la
estructura gramatical del flujo de tokens. Una representación típica es el árbol
sintáctico, en el cual cada nodo interior representa una operación y los hijos del nodo
representan los argumentos de la operación.
En nuestro modelo de compilador, el analizador sintáctico obtiene una cadena de
tokens del analizador léxico, como se muestra en la figura 4.1, y verifica que la cadena
de nombres de los tokens pueda generarse mediante la gramática para el lenguaje
fuente. Esperamos que el analizador sintáctico reporte cualquier error sintáctico en
forma inteligible y que se recupere de los errores que ocurren con frecuencia para
seguir procesando el resto del programa. De manera conceptual, para los programas
bien formados, el analizador sintáctico construye un árbol de análisis sintáctico y lo
pasa al resto del compilador para que lo siga procesando. De hecho, el árbol de análisis
sintáctico no necesita construirse en forma explícita, ya que las acciones de
comprobación y traducción pueden intercalarse con el análisis sintáctico, como
veremos más adelante.
Por ende, el analizador sintáctico y el resto de la interfaz de usuario podrían
implementarse sin problemas mediante un solo módulo.

Existen tres tipos generales de analizadores para las gramáticas: universales,


descendentes y ascendentes. Los métodos universales de análisis sintáctico como el
algoritmo de Cocke-Younger-Kasami y el algoritmo de Earley pueden analizar cualquier
gramática (vea las notas bibliográficas).
Sin embargo, estos métodos generales son demasiado ineficientes como para usarse
en la producción de compiladores.
Los métodos que se utilizan, por lo regular, en los compiladores pueden clasificarse
como descendentes o ascendentes. Según sus nombres, los métodos descendentes
construyen árboles de análisis sintáctico de la parte superior (raíz) a la parte inferior
(hojas), mientras que los métodos ascendentes empiezan de las hojas y avanzan hasta
la raíz. En cualquier caso, la entrada al analizador se explora de izquierda a derecha, un
símbolo a la vez.
Recordemos que el analizador sintáctico o parser es el corazón del compilador o
intérprete y gobierna todo el proceso. Su objetivo es realizar el resto del análisis
(continuando el trabajo iniciado por el analizador morfológico) para comprobar que la
sintaxis de la instrucción en cuestión es correcta. Para ello, el analizador sintáctico
considera como símbolos terminales las unidades sintácticas devueltas por el
analizador léxico.

La separación del análisis léxico y el análisis sintáctico a menudo nos permite


simplificar por lo menos una de estas tareas. Por ejemplo, un analizador sintáctico que
tuviera que manejar los comentarios y el espacio en blanco como unidades sintácticas
sería mucho más complejo que uno que asumiera que el analizador léxico ya ha
eliminado los comentarios y el espacio en blanco. Si vamos a diseñar un nuevo
lenguaje, la separación de las cuestiones léxicas y sintácticas puede llevar a un diseño
más limpio del lenguaje en general.

Ejemplo de analizador sintáctico

El árbol tiene un nodo interior etiquetado como *, con (id, 3) como su hijo izquierdo, y
el entero 60 como su hijo derecho. El nodo (id, 3) representa el identificador velocidad.
El nodo etiquetado como * hace explicito que primero debemos multiplicar el valor de
velocidad por 60. El nodo etiquetado como + indica que debemos sumar el resultado
de esta multiplicación al valor de inicial. La raíz del árbol, que se etiqueta como =,
indica que debemos almacenar el resultado de esta suma en la ubicación para el
identificador posicion. Este ordenamiento de operaciones es consistente con las
convenciones usuales de la aritmética, las cuales nos indican que la multiplicación
tiene mayor precedencia que la suma y, por ende, debe realizarse antes que la suma.

Análisis Semántico
El analizador semántico utiliza el árbol sintáctico y la información en la tabla de
símbolos para comprobar la consistencia semántica del programa fuente con la
definición del lenguaje. También recopila información sobre el tipo y la guarda, ya sea
en el árbol sintáctico o en la tabla de símbolos, para usarla más tarde durante la
generación de código intermedio.
Una parte importante del análisis semántico es la comprobación (verificación) de tipos,
en donde el compilador verifica que cada operador tenga operandos que coincidan.
Por ejemplo, muchas definiciones de lenguajes de programación requieren que el
índice de un arreglo sea entero; el compilador debe reportar un error si se utiliza un
número de punto flotante para indexar el arreglo.
La especificación del lenguaje puede permitir ciertas conversiones de tipo conocidas
como coerciones. Por ejemplo, puede aplicarse un operador binario aritmético a un
par de enteros o a un par de números de punto flotante. Si el operador se aplica a un
número de punto flotante y a un entero, el compilador puede convertir u obligar a que
se convierta en un número de punto flotante.

Comprobación semántica
la salida que genera el análisis semántico, en el caso de que no haya detectado errores,
es un árbol de derivación con anotaciones semánticas. Dichas anotaciones se pueden
usar para comprobar que el programa es semánticamente correcto, de acuerdo con las
especificaciones del lenguaje de programación. Hay que comprobar, por ejemplo, que:
 Cuando se utiliza un identificador, éste ha sido declarado previamente.
• Se ha asignado valor a las variables antes de su uso.
• Los índices para acceder a los arrays están dentro del rango válido.
• En las expresiones aritméticas, los operandos respetan las reglas sobre los tipos
de datos permitidos por los operadores.
• Cuando se invoca un procedimiento, éste ha sido declarado adecuadamente.
Además, el número, tipo y posición de cada uno de sus argumentos debe ser
compatible con la declaración.
• Las funciones contienen al menos una instrucción en la que se devuelve su valor
al programa que las invocó.

Ejemplo de analizador semántico

Suponga que posicion, inicial y velocidad se han declarado como números de punto
flotante, y que el lexema 60 por sí solo forma un entero. El comprobador de tipo en el
analizador semántico descubre que se aplica el operador * al número de punto
flotante velocidad y al entero 60. En este caso, el entero puede convertirse en un
número de punto flotante. Observe en la figura 1.7 que la salida del analizador
semántico tiene un nodo adicional para el operador inttofloat, que convierte de
manera explícita su argumento tipo entero en un número de punto flotante. En el
capítulo 6 hablaremos sobre la comprobación de tipos y el análisis semántico.

Generación de código intermedio


En el proceso de traducir un programa fuente a código destino, un compilador puede
construir una o más representaciones intermedias, las cuales pueden tener una
variedad de formas. Los árboles sintácticos son una forma de representación
intermedia; por lo general, se utilizan durante el análisis sintáctico y semántico.
Después del análisis sintáctico y semántico del programa fuente, muchos compiladores
generan un nivel bajo explícito, o una representación intermedia similar al código
máquina, que podemos considerar como un programa para una máquina abstracta.
Esta representación intermedia debe tener dos propiedades importantes: debe ser
fácil de producir y fácil de traducir en la máquina destino.
Optimización de código
La fase de optimización de código independiente de la máquina trata de mejorar el
código intermedio, de manera que se produzca un mejor código destino. Por lo
general, mejor significa más rápido, pero pueden lograrse otros objetivos, como un
código más corto, o un código de destino que consuma menos poder. Por ejemplo, un
algoritmo directo genera el código intermedio (1.3), usando una instrucción para cada
operador en la representación tipo árbol que produce el analizador semántico.
Un algoritmo simple de generación de código intermedio, seguido de la optimización
de código, es una manera razonable de obtener un buen código de destino. El
optimizador puede deducir que la conversión del 60, de entero a punto flotante,
puede realizarse de una vez por todas en tiempo de compilación, por lo que se puede
eliminar la operación inttofloat sustituyendo el entero 60 por el número de punto
flotante 60.0.

Generación de código
El generador de código recibe como entrada una representación intermedia del
programa fuente y la asigna al lenguaje destino. Si el lenguaje destino es código
máquina, se seleccionan registros o ubicaciones (localidades) de memoria para cada
una de las variables que utiliza el programa. Después, las instrucciones intermedias se
traducen en secuencias de instrucciones de máquina que realizan la misma tarea. Un
aspecto crucial de la generación de código es la asignación juiciosa de los registros
para guardar las variables.
Bibliografía

 Ceballos Sierra, J. (2015). Visual Basic.NET: lenguaje y aplicaciones (3a. ed.)..


RA-MA Editorial. https://elibro.net/es/lc/utelvt/titulos/59726
 Cerezo López, Y. Peñalba Rodríguez, O. y Caballero Roldán, R. (2007).
Iniciación a la programación en C#: un enfoque práctico. Las Rozas (Madrid),
Delta Publicaciones. Recuperado de
https://elibro.net/es/ereader/utelvt/169705?page=16 .
 Aho Alfred, et al. (2008). Compiladores, principios, técnicas y herramientas
(2ª. Ed.). México, Pearson Education.
 Alfonseca Manuel, et al. (2006). Compiladores e intérpretes: teoría y
práctica. Universidad Autónoma de Madrid. España, Pearson Education.

También podría gustarte