2. “El propósito primario de un lenguaje de
programación es ayudar al programador en la
práctica de su arte”
Charles Hoare
3. 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.
4. 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:
6. 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.
7. 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.
8. 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.
9. 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.
10. 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.
11. 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.
12. 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.
13. 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.
14. 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.
15. 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.
16. 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.
17. 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.
18. 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.
19. 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.
20. 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.
21. 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;
}
22. 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)))))
23. 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).
24. 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.
25. 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.
26. 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:
27. 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.
28. 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.
29. 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.
30. 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:
31. 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).
32. 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.
33. 1.4 Compiladores e Intérpretes
Ejemplo: compilación en Fortran, se asemeja a la
compilación “pura”.
34. 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
35. 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:
36. 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.
37. 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.
38. 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.
39. 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.
40. 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.
42. 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.
43. 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).
44. 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.
45. 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);
}
46. 1.6.1 Análisis Léxico y Sintáctico
int main ( )
{ int i =
getint ( ) ,
j = getint (
) ; while (
i != j )
47. 1.6.1 Análisis Léxico y Sintáctico
{ if ( i
> j ) i
= i - j
; else j =
j - i ;
} putint ( i
) ; }
48. 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.
49. 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).
51. 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.
52. 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.
53. 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.
54. 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.
55. 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.
56. 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.
57. 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.
58. 1.6.2 Análisis Semántico y
Generación de Código Intermedio
Ejemplo de árbol sintáctico abstracto:
59. 1.6.2 Análisis Semántico y
Generación de Código Intermedio
La tabla de símbolos para el árbol sería:
60. 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.
61. 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.
62. 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.