introducción a lenguajes de programacion

62
Capítulo 1. Introducción Raúl José Palma Mendoza

Transcript of introducción a lenguajes de programacion

Capítulo 1. Introducción

Raúl José Palma Mendoza

“El propósito primario de un lenguaje de programación es ayudar al programador en la

práctica de su arte”Charles Hoare

Introducción

Las primeras computadoras eran artefactos monstruosos: grandes, costosas y con el poder de procesamiento de un calculadora.

Los programadores creían que el tiempo de éstas era más importante que el de ellos mismos.

Ellos programaban usando lenguaje de máquina.

Introducción

Ejemplo de un programa en lenguaje de máquina para calcular el máximo común divisor de dos enteros el procesador MIPS R4000:

Introducción

Introducción

Los lenguajes ensambladores se diseñaron originalmente para tener una correspondencia uno a uno entre los mnemónicos y las instrucciones de lenguaje de máquina.

Se le llamó “ensamblador” al software de sistema encargado de hacer la traducción.

Luego se les añadió capacidades de expasión de macros que permitían definir abreviaciones parametrizadas para secuencias de instrucciones comunes.

Introducción

Era necesario escribir programas nuevos para cada máquina nueva que aparecía.

Las personas empezaron a desear un lenguaje independiente de la máquina, en el cual los cálculos numéricos fuesen expresados de forma más similar a al lenguaje matemático.

Fua hasta mediados de los 50's que se desarrolló el dialecto original de Fortran, el primer lenguaje de programación de alto nivel.

Poco después aparecieron Lisp y Algol.

Introducción

Traducir de un lenguaje de alto nivel a lenguaje ensamblador o lenguaje de máquina es el trabajo del compilador.

Los compiladores son mucho más complejos que los ensambladores, pues ya no existe la relación uno a uno entre el lenguaje de alto nivel y el lenguaje de máquina.

Los lenguajes de alto nivel también se pueden interpretar, por ejemplo: Phyton y Javascript.

Introducción

Al inicio era más eficiente escribir directamente en lenguaje de máquina que compilar, pero ahora los compiladores han avazando tanto que en general producen código ensamblador más eficiente, que lo que puede producir un ser humano.

1.1 El Arte del Diseño de Lenguajes

Actualmente hay miles de lenguajes de alto nivel, por razones como:

Evolución. Las ciencias de la computación son una disciplina joven en constante evolución.

Propósitos especiales. Hay varios lenguajes que se desarrollan para problemas específicos.

Preferencias personales. A las personas les gustan diferentes cosas: brevedad, recursividad, iteración, punteros, etc. todo esto hace improbable que algún día exista un lenguaje universal.

1.1 El Arte del Diseño de Lenguajes

Aunque hay miles ¿qué características contribuyen a que un lenguaje sea ampliamente usado? Potencia expresiva. Escribir código claro,

conciso y mantenible. Facilidad de uso para el novato. Tener una

curva de aprendizaje baja, ej.: Basic, Logo, Pascal.

Facilidad de implementación. Ej.: Basic, Pascal.

1.1 El Arte del Diseño de Lenguajes

Estandarización. Casi todos los lenguajes ampliamente usados tienen un estándar internacional oficial. Pascal falló en eso al no estandarizar elementos como las cadenas y la compilación separada. Algol 60 también falló al no estandarizar una librería de entrada y salida.

Código Abierto. Muchos lenguajes ampliamente usados tienen un compilador o intérprete de código abierto en la actualidad. C se creó para implementar Unix y también actualmente se usa para Linux.

1.1 El Arte del Diseño de Lenguajes

Excelentes compiladores. Los compiladores de Fortran son famosos por su eficiencia, otros lenguajes como Common Lisp tienen compiladores y herramientas que ayudan mucho al manejo de proyectos grandes.

Economía y Patrocinio. Muchos lenguajes permanecen debido al respaldo de fuertes empresas y a que reemplazarlos sería muy costos. Ej.: Ada debe su vida al DoD (U.S. Department of Defense), pues es complejo de implementar, C# debe su gran aceptación a Microsoft.

1.1 El Arte del Diseño de Lenguajes

No hay un sólo factor que determine que un lenguaje es bueno, hay que considerar el punto de vista del implementador y del programador.

El primero está interesado en cómo decirle a la computadora qué hacer, y el segundo en cómo expresar sus algoritmos e ideas. Al inicio el primer punto de vista era dominante.

En 1984 Donald Knuth sugirió que la programación debería de ser reconocida como el arte de decirle a otro humano qué es lo que uno desea que la computadora haga. Declarando así que la claridad conceptual y la eficiencia de implementación son muy importantes.

1.2 El Abanico de los Lenguajes

Podemos hacer varias clasificaciones y someterlas a discusión en nuestro caso tomaremos las siguiente:

Declarativos Funcionales: Lisp/Scheme, ML, Haskell. Flujo de Datos: Id, Val, SISAL. Lógicos o basados en restricciones: Prolog, en

ocasiones también se considera a SQL, a lenguajes basados en plantillas como XSLT y aspectos programables de las hojas de cálculo en esta categoría.

1.2 El Abanico de los Lenguajes

Imperativos von Neumann: C, Ada, Fortran, ... Scripting: Perl, Python, PHP, … Orientados a objetos: Smalltalk, Eiffel, Java, …

Los lenguajes funcionales utilizan modelo computacional basado en la definición recursiva de funciones, toman su inspiración en el cálculo lambda un modelo computacional formal desarrollado por Alonzo Church en los 1930's.

1.2 El Abanico de los Lenguajes

Los lenguajes de flujo de datos modelan la computación como un flujo de información a través de nodos funcionales primitivos, este modelo es inherentemente paralelo, los nodos se activan al recibir datos (tokens) y pueden operar concurrentemente.

Los lenguajes lógicos o basados en restricciones toman su inspiración en la lógica de predicados, modelan la computación como un intento de encontrar valores que satisfagan ciertas relaciones especificadas, usando una búsqueda diriga por metas.

1.2 El Abanico de los Lenguajes

En ocasiones se clasifica dentro de los lenguajes lógicos o basados en restricciones al lenguaje SQL, al lenguaje XSLT y a algunos aspectos programables de hojas de cálculo como Excel.

Los lenguajes de von Neumann son los más usados, modelan la computación como serie de cambios de estado o de modificaciones de variables. Están basados en enunciados secuenciales que influencian al siguiente usando efectos de lado.

1.2 El Abanico de los Lenguajes

Los lenguajes de scripting son un subconjunto de los lenguajes de von Neumann hacen énfasis en pegar o conectar componentes hechos en diferentes lenguajes, no hacen énfasis en la eficiencia, sino en la brevedad de escritura, generalmente son interpretados.

Los lenguajes orientados a objetos en general están estrechamente relacionados con los lenguajes de von Neumann pero tienen un modelo de computación y de la memoria mucho más estructurado y distribuido.

1.2 El Abanico de los Lenguajes

De los lenguajes orientados a objetos el “más puro” es Smalltalk, C++ y Java probablemente los más usados.

Podríamos pensar en que los lenguajes concurrentes son una clase independiente, pero lo ocurre es que la concurrencia se investiga e implementa en cada una de las clases mencionadas.

1.2 El Abanico de los Lenguajes

Ahora veamos tres ejemplos para encontrar el máximo común divisor (gcd) en tres lenguajes distintos:

int gcd(int a, int b) { // C

while (a != b) {

if (a > b) a = a - b;

else b = b - a;

} return a;

}

1.2 El Abanico de los Lenguajes

; Scheme

(define gcd

(lambda (a b)

(cond ((= a b) a)

((> a b) (gcd (- a b) b))

(else (gcd (- b a) a)))))

1.2 El Abanico de los Lenguajes

% Prolog

gcd(A,B,G) :- A = B, G = A.

gcd(A,B,G) :- A > B, C is A-B, gcd(C,B,G).

gcd(A,B,G) :- B > A, C is B-A,

gcd(C,A,G).

1.3 ¿Por qué Estudiar Lenguajes de Programación?

Para saber seleccionar el lenguaje adecuado recordando la frase de Charles Hoare.

Para aprender nuevos lenguajes fácilmente. Para saberlos aprovechar mejor al conocerlos

internamente.

1.3 ¿Por qué Estudiar Lenguajes de Programación?

Algunos de los beneficios son: Entender características oscuras. Escoger correctamente entre formas alternativas de

expresar las cosas. Hacer buen uso de los depuradores, enlazadores,

ensambladores. Simular características útiles de un lenguaje en

otro. Hacer uso de la tecnología de lenguajes donde sea

que aparezca.

1.4 Compiladores e Intérpretes

Un compilador traduce un programa en código fuente a un programa equivalente en un código meta (usualmente en lenguaje de máquina). Y luego termina su ejecución:

1.4 Compiladores e Intérpretes

Un compilador por sí mismo es un programa en lenguaje de máquina, probablemente creado al compilar otro lenguaje de alto nivel.

Cuando el lenguaje de máquina ha sido escrito en un formato de archivo entendible para el sistema operativo se le conoce como código objeto.

1.4 Compiladores e Intérpretes

A diferencia de un compilador, un intérprete permanece ejecutando durante la ejecución del software, éste implementa una máquina virtual cuyo lenguaje de máquina es el lenguaje de programación de alto nivel que se está interpretando.

1.4 Compiladores e Intérpretes

La interpretación generalmente conlleva a tener mayor flexibilidad y mejores diagnósticos de errores que la compilación, algunas características son casi imposibles de obtener sin interpretación, ej.: los programas que pueden añadir nuevas líneas de código a sí mismos durante la ejecución y ejecutárlas también.

En cambio la compilación generalmente lleva a una mayor eficiencia.

1.4 Compiladores e Intérpretes

En la práctica ocurre que muchas implementaciones de lenguajes incluyen una mezcla de compilación e interpretación:

1.4 Compiladores e Intérpretes

Para el caso anterior, diríamos que el lenguaje es interpretado si la traducción inicial es “sencilla” y si es compleja, si incluye un análisis completo y el programa intermedio no tiene parecido al programa fuente, entonces decimos que es compilado.

La discusión surge en lenguajes como Java que tiene una traducción inicial compleja y luego tiene una interpretación compleja. Aunque en las últimas versiones de Java se está dejando la interpretación por la “compilación justo a tiempo” (Just In Time).

1.4 Compiladores e Intérpretes

Preprocesamiento: Ocurre como un paso previo en la mayoría de los

lenguajes interpretados (ej.: Lisp), un preprocesador, remueve comentarios, espacios en blanco y agrupa caracteres en “tokens” como palabras clave, identificadores, números y símbolos, también podría expandir macros, e incluso indentificar estructuras sintácticas de alto nivel como ciclos y subrutinas, con el fin de obtener un código más eficiente para interpretar.

1.4 Compiladores e Intérpretes

Ejemplo: compilación en Fortran, se asemeja a la compilación “pura”.

1.4 Compiladores e Intérpretes

Ejemplo: ensamblado posterior a la compilación, este libera al compilador de cambios en el lenguaje de máquina, un mismo ensamblador puede ser usado por varios compiladores

1.4 Compiladores e Intérpretes

Ejemplo: el preprocesador de C, remueve comentarios y expande macros, además puede ser instruído para borrar partes del código, proveyendo un compilación condicional con el mismo código:

1.4 Compiladores e Intérpretes

Ejemplo: traducción de código fuente en C++ a código fuente en C. Es una compilación completa pues el compilador de C++ hace una análisis exhaustivo y provoca un cambio significativo (no directo) en el código.

1.4 Compiladores e Intérpretes

Bootstrapping Muchos compiladores están escritos en el

lenguaje que compilan, para lograr esto se usa una técnica conocida como “bootstrapping”, nombrada debido a la frase en inglés: “pull oneself up by one's bootstraps”.

Básicamente consiste en crear un pequeña implementación del lenguaje y usar esta para ir creando otras más complejas.

1.4 Compiladores e Intérpretes

Ejemplo: Si quisierámos empezar a construir el primer

compilador de Java y tuviésemos ya un compilador de C, podríamos iniciar escribiendo un pequeño compilador para un subconjunto de Java en C usando un pequeño subconjunto de C.

Luego podríamos desarrollar usando el subconjunto de Java y compilar el pequeño compilador de Java en el compilador que ya tenemos y después podríamos usar este compilador de Java escrito en Java para desarrollar y compilar un compilador que acepte un subconjunto mayor de Java y así sucesivamente.

1.4 Compiladores e Intérpretes

Los compiladores no necesariamente traducen de un código de alto nivel a un lenguaje de máquina, existen compiladores que traducen una descripción de un documento de texto en comando para una impresora, una consulta SQL en operaciones primitivas sobre archivos, un diseño del ambiente de un edificio a un lenguaje entendible por un motor de 3D, etc.

1.5 Ambientes de Programación

Además de los compiladores e intépretes los programadores son asistidos en sus tareas por otras herramientas como: ensambladores, depuradores, preporcesadores, enlazadores, editores de texto, entre otros.

Anteriormente estas herramientas se ejecutaban individualmente, pero en la actualidad estas se integran cada vez más en los conocidos Ambientes de Desarollo Integrado o IDE por sus siglas en inglés. Por ejemplo: Eclipse, Netbeans, Visual Studio.

1.6 Un Vistazo al Proceso de Compilación

1.6 Un Vistazo al Proceso de Compilación

Los primeros pasos hasta el análisis semántico inclusive sirven para determinar el significado del programa, se le conoce como el front end del compilador, la últimos pasos sirven para encontrar un programa equivalente en el código meta, se conocen como el back end del compilador.

Los compiladores están divididos en “pasos” que deben realizarse uno después del otro, esto permite que varios compiladores para distintos códigos fuente compartan el mismo back end y varios compiladores para distintos lenguajes de máquina compartan el mismo front end.

1.6.1 Análisis Léxico y Sintáctico

El análisis léxico también llamado escaneo y el análisis sintáctico también llamado parseo, sirven para determinar la estructura del programa.

El escáner (analizador léxico) lee los caracteres del programa en código fuente y los agrupa en tokens, que son la unidad con sentido más pequeña que tienen los programas. La principal razón de hacer esto es simplificar el trabajo del parser (analizador sintáctico).

1.6.1 Análisis Léxico y Sintáctico

Usualmente el escáner remueve espacios en blanco innecesarios, remueve comentarios y etiqueta los tokens con números de línea y de columna para poder hacer diagnósticos acertados en fases posteriores.

1.6.1 Análisis Léxico y Sintáctico

Ejemplo: Dado el siguiente programa en C (gcd), sus tokens muestran a continuación:

int main() {

int i = getint(), j = getint();

while (i != j) {

if (i > j) i = i - j;

else j = j - i;

} putint(i);

}

1.6.1 Análisis Léxico y Sintáctico

int main ( )

{ int i =

getint ( ) ,

j = getint (

) ; while (

i != j )

1.6.1 Análisis Léxico y Sintáctico

{ if ( i

> j ) i

= i - j

; else j =

j - i ;

} putint ( i

) ; }

1.6.1 Análisis Léxico y Sintáctico

El parseo crea un árbol de parseo que representa contrucciones de mayor nivel que los tokens ( ej.: enunciados, expresiones, subrutinas, etc.) en términos de otras construcciones y/o tokens.

Cada construcción es un nodo en el árbol, los elementos que la conforman son sus hijos, la raíz del árbol es la construcción “programa”; las hojas siempre son tokens recibidos del escáner. Visto de forma completa el árbol muestra como los tokens forman un programa válido.

La estructura del árbol se basa en una serie de reglas potencialmente recursivas conocidas como una gramática libre de contexto.

1.6.1 Análisis Léxico y Sintáctico

Decimos que la sintaxis del lenguaje está definida por una gramática libre de contexto, por ejemplo hay infinitas grámaticas libres de contexto para definir C, a continuación veremos un ejemplo de un árbol de parseo para la gramática de C oficial del estándar de 1999.

La líneas punteadas representan una cadena de reemplazos uno a uno, el número contiguo a la línea representa el número de nodos omitidos, (esto se hace para ahorrar espacio).

1.6.1 Análisis Léxico y Sintáctico

1.6.1 Análisis Léxico y Sintáctico

En el proceso de escaneo y parseo el compilador revisa que todos los tokens estén bien formados y que su secuencia esté conforme a la sintaxis definida en la gramática libre de contexto, cualquier error deberá ser informado.

1.6.2 Análisis Semántico y Generación de Código Intermedio

En el análsis semántico se descubre el significado del programa.

Se analiza si varias ocurrencias del mismo identificador corresponden al mismo elemento del programa.

En la mayoría de los lenguajes lleva un control de los tipos de los identificadores y de las expresiones.

Genera y mantiene una tabla de símbolos que relaciona cada identificador con la información que se va obteniendo de él.

1.6.2 Análisis Semático y Generación de Código Intermedio

Por ejemplo en C, el analizador semántico lleva el control de:

Cada identificador sea declarado antes de ser usado.

Que cada identificador sea usando en un contexto apropiado, (ej.: llamar a un entero como subrutina, sumar una cadena a un real, etc.)

Que los llamados a subrutinas tengan el número y tipo correcto en los argumentos.

Que las etiquetas de las ramas de un switch sean distintas.

Etc.

1.6.2 Análisis Semático y Generación de Código Intermedio

En muchos compiladores, el trabajo del analizador semántico toma la forma de rutinas de acción semántica, invocada por el parser cuando se da cuenta que ha llegado a un punto en particular dentro de una regla gramatical.

No todas las reglas semánticas se puede comprobar en tiempo de compilación:

Las que pueden comprobar se conocen como semática estática del lenguaje.

La que se deben comprobar en tiempo de ejecución se refieren como la semántica dinámica del lenguaje.

1.6.2 Análisis Semántico y Generación de Código Intermedio

Un ejemplo de semántica dinámica común en muchos lenguajes sería:

Verificar que las variables no se usan en una expresión, a menos que se les ha dado un valor.

Verificar que los punteros no se desreferencian, a menos que se refieren a un objeto válido.

Verificar que el subíndice se encuentra dentro de los límites de la matriz.

Verificar que las operaciones aritméticas no provoquen un desbordamiento.

1.6.2 Análisis Semántico y Generación de Código Intermedio

Un árbol de parseo se conoce a veces como un árbol de sintaxis concreto, porque demuestra por completo y en concreto, cómo una determinada secuencia de tokens se pueden derivar conforme a las reglas de la gramática libre de contexto. Sin embargo, una vez que sabemos que una secuencia de tokens es válido, mucha de la información en el árbol de análisis es irrelevante para las fases posteriores de la compilación.

1.6.2 Análisis Semántico y Generación de Código Intermedio

El analizador semántico por lo general transforma el árbol de parseo en un árbol de sintaxis abstracta (también conocido como un AST, o simplemente un árbol sintáctico) mediante la eliminación de la mayor parte de los nodos “artificiales” en el interior del árbol. El analizador semántico también anota la información útil de los nodos restantes, como los punteros de los identificadores a sus entradas en la tabla de símbolos.

1.6.2 Análisis Semántico y Generación de Código Intermedio

Ejemplo de árbol sintáctico abstracto:

1.6.2 Análisis Semántico y Generación de Código Intermedio

La tabla de símbolos para el árbol sería:

1.6.2 Análisis Semántico y Generación de Código Intermedio

En algunos compiladores el árbol sintáctico y la tabla de símbolos corresponden a la forma intermedia que se pasa del front end al back end.

En otros esta forma intermedia se obtiene a partir de un recorrido de dicho árbol para generar un grafo de control de flujo cuyos nodos se asemejan a fragmentos de un lenguaje ensamblador para una máquina idealizada.

1.6.3 Generación del Código Meta

Para generar el código meta, el generador de código usa la tabla de símbolos para asignar lugares a las variables, y luego atraviesa la representación intermedia del programa, generando las cargas y almacenamientos para las referencias a variables, intercaladas con las operaciones aritméticas correspondientes, decisiones y ramificaciones

Usualmente el generador de código almacenará la tabla de símbolos para ser usada por un depurador simbólico, incluyéndola en el código meta.

1.6.4 Mejoras al Código

Usualmente se les llama optimizaciones, aunque en realidad en raras ocasiones se logra obtener un resultado óptimo.

Es una fase opcional de la compilación que busca que el código sea más eficiente.

Puede haber optimizaciones independientes de la máquina que suelen hacerse sobre la representación intermedia.

Las optimizaciones dependientes de la máquina se hacen a través de transformaciones al código meta.