1 / 213

CURSO: PROGRAMACION DE SISTEMAS

CURSO: PROGRAMACION DE SISTEMAS. ELEMENTOS DEL CURSO. Programa del Curso..... PDF Datos del Profesor …. RZC Contenido del Curso…. Diaps Proyectos …. Archs Software y Herramientas. Unidad I Introducción a la Programación de Sistemas. 1.1 ¿Qué es y que estudia la P. de S. ?.

abiba
Télécharger la présentation

CURSO: PROGRAMACION DE SISTEMAS

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. CURSO: PROGRAMACION DE SISTEMAS

  2. ELEMENTOS DEL CURSO • Programa del Curso..... PDF • Datos del Profesor …. RZC • Contenido del Curso…. Diaps • Proyectos …. Archs • Software y Herramientas

  3. Unidad I Introducción a la Programación de Sistemas

  4. 1.1 ¿Qué es y que estudia la P. de S. ? • Son los programas que residen en un sistema de computación. Su función es proporcionar al usuario o programador una interfase mas eficiente y practica con relación al hardware de la maquina. La P. de S. estudia como están implementados cada uno de los programas de un Sistema (ver notas).

  5. Ejemplos:       Compiladores (javac)       Ensambladores (Masm)       Interpretes (Visual Basic)       Ligadores (Link)       Cargadores       Sistema Operativo (Windows)       Utilerías de Sistemas (Debugger) 1.2 Herramientas desarrolladas con la P de S

  6. 1.3 Lenguajes • Naturales (Traductores de Ingles-Español, Ingles-Ruso, etc) • Artificiales (Compiladores de LP como Java, C++, Ada, etc.)

  7. 1.4 Traductor y su Estructura • Ensambladores …. (notas) • Compiladores……. (notas) • Intérpretes……...... (notas)

  8. 1.5 Generador de Código para Compiladores (Compilador de Compiladores) • Definición: Un compilador de compiladores o generador de Parsers es una utilería para generar el código fuente de un parser, intérprete o compilador a partir de una descripción de lenguaje anotado en la forma de gramática (usualmente BNF) mas código que es asociado con cada una de las reglas de la gramática que debe ser ejecutada cuándo esas reglas sean aplicadas por el parser. Esas piezas de código son algunas veces llamadas rutinas de acciones semánticas ya que ellas definen la semántica de las estructura sintáctica y que es analizada por el parser. Dependiendo del tipo de parser que será generado, las rutinas pueden construir un árbol de parsing (o AST) o generar código ejecutable directamente (notas).

  9. Unidad IIIntroducción al Diseño de Lenguajes de Programación

  10. Visión del Problema. • Consideraciones Preliminares. • Objetivos y filosofías del diseño de los lenguajes de programación. • Diseño detallado. • Caso de estudio.

  11. Unidad III Análisis de Léxico

  12. 3.1 Introducción a los Autómatas Finitos y Expresiones Regulares • (ver apuntes de Lenguajes y Autómatas).

  13. 3.2 Analizador de Léxico. • Descompone la entrada en palabras individuales llamadas “tokens”. El analizador de léxico (scanner) toma una cadena de caracteres que forman la entrada y produce una cadena de nombres o identificadores, palabras claves o reservadas (PC) signos o marcas de puntuación, operadores aritméticos y lógicos, constantes (números, cadenas, etc.) y otros. También el scanner tiene como función desechar espacios en blanco y comentarios entre los tokens, otra función podría ser crear la tabla de símbolos. Ejemplo: TIPO EJEMPLOS ID foo n14 last NUM 73 0 00 515 REAL 66.1 .5 10. 1e67 IF if COMMA , NOTEQ != LPAREN (

  14. Analizador de Léxico (cont.) Tokens de puntuación como IF, VOID y RETURN son llamadas palabras reservadas y en la mayoría de los lenguajes no pueden usarse como identificadores.

  15. 3.3 Manejo de “Buffers” • El analizador de léxico (scanner) y el analizador de sintáxis (parser) forman un duo “productor-consumidor”. El scanner produce tokens y el parser los consume. La implementación de la lectura de los caracteres de la entrada (disco) es usualmente hecha por un buffer (memoria) que contendrá una buena parte de la entrada, desde donde el scanner irá formando los tokens. Esto agiliza la entrada que de otra forma leería carácter por carácter desde el disco.

  16. 3.4 Creación de la Tabla de Símbolos. • Checar notas La tabla de símbolos es una estructura de datos muy importante en casi todo el proceso de compilación. En ella se guarda durante las primeras fases de compilación los nombres de los identificadores (símbolos) usados en el programa fuente, además de los atributos de cada uno de estos identificadores. Estos identificadores y símbolos junto con sus atributos serán usados posteriormente para realizar funciones como el chequeo de tipos, la asignación de memoria, generación de código objeto etc.

  17. Ejemplo Ejemplo.- Programa X1;          Var X, Y: Integer;                 Z: Real;                 Arreglo: Array [1…100] of int          Procedure compara (a, b: Integer)                    Var n, m: integer;                    Begin ---- ---- End Begin ---- ---- End

  18. 3.5 Manejo de Errores de Léxico • Errores posibles detectados en el análisis de léxico son: • Patrones de tokens que no coincidan con algún patrón válido. Por ejemplo el token #### sería inválido en algunos L.P. • Caracteres inválidos para el alfabeto del el lenguaje. • Longitud de ciertos tokens demasiado larga.

  19. 3.6 Generadores de Código Léxico • La construcción de un scanner puede automatizarse por medio de un generador de analizadores de léxico. • El primer programa de este tipo que se hizo popular fue Lex (Lesk, 1975) que generaba un scanner en lenguaje C. • Hoy en día existen muchos programas de este tipo: Flex, Zlex, YooLex, JavaCC, SableCC, etc.

  20. Ejemplo: Javacc • Javacc (Java Compiler Compiler) es un generador de scanners y parsers (https://javacc.dev.java.net/). • Toma como entrada especificaciones de léxico (expresiones regulares) y sintáxis (gramática de contexto libre) y produce como salida un analizador de léxico y un parser recursivo decendente. • Se puede usar como pre-procesador de Javacc, a jjtree y a JTB para la construcción del árbol sintáctico.

  21. (cont.) USO DE JAVACC Miparser.jj javacc Miparser.jj Miparser.java ParseException.java TokenMgrError.java otros archivos java javac Miparser.java Miparser.class java Miparser <inputfile mensajes

  22. (cont.) EJEMPLO DE ESPECIFICACION PARA GENERAR UN SCANNER PARSER_BEGIN(PS1) class PS1 {} PARSER_END(PS1) /* Para la expresión regular de la derecha lo de la izquierda será retornado */ TOKEN: { <IF: "if"> |<#DIGIT: ["0"-"9"]> |<ID: ["a"-"z"] (["a"-"z"]|<DIGIT>)*> |<NUM: (<DIGIT>)+> |<REAL: ((<DIGIT>)+ "." (<DIGIT>)*) | ((<DIGIT>)* "." (<DIGIT>)+)> } SKIP: { <"--" (["a" - "z"])* ("\n" | "\r" | "\r\n")> |" " |"\t" |"\n" |"\r" } void Start(): {} { (<IF> | <ID> | <NUM> | <REAL>)* }

  23. Unidad IVAnálisis de Sintáxis

  24. 4.1 Introducción a las gramáticas Libres de Contexto y Árboles de derivación • El tipo de gramáticas usadas en los LP son llamadas gramáticas de contexto libre, las cuáles, junto con árboles de derivación, fueron estudiadas en el curso de Lenguajes y Autómatas. • Un ejemplo de una gramática para un LP simple es la siguiente: 1) SS;S 2) Sid := E 3) Sprint (L) 4) E id 5) E num 6) E E + E 7) E(S,E) 8) L E 9) L L , E • (ver ejemplo de derivaciones y árboles de parsing) • A continuación veremos un ejemplo de una gramática para Java en formato BNF (liga).

  25. (cont.) • Gramática Ambigua. Una gramática es ambigua si puede derivar una oración (cadena) con dos diferentes árboles de parsing. La sig. Gramática es ambigüa: Eid Enum EE*E EE/E EE+E EE-E E(E) ya que tiene dos árboles de parsing para la misma oración (árboles para id:=id+id+id con primera gramática y árboles para 1-2-3 con segunda en sig “slice”).

  26. S Id := E E + E E + E id Id id (cont.) S Id := E E + E id E + E id id

  27. (cont.) E E - E E - E 3 1 2 E E - E 1 E - E 2 3

  28. 4.2 Diagramas de Sintaxis • Un método alternativo al BNF para desplegar las producciones de ciertas gramáticas es el diagrama de sintaxis. Ésta es una imagen de la producciones que permite al usuario ver las sustituciones en forma dinámica, es decir, verlas como un movimiento a través del diagrama. Ejemplo en Java (liga)

  29. 4.3 Precedencia de Operadores • En los dos ejemplos vistos en sección 4.1, los dos árboles de parsing para 1-2-3 significaba diferentes cosas: (1-2)-3=-4 versus 1-(2-3)=2. Similarmente, (1+2)x3 no es lo mismo que 1+(2x3). Es por eso que gramáticas ambiguas son problemáticas para un compilador. Afortunadamente, una gramática ambigua puede convertirse en una no ambigua. Por ejemplo, si queremos encontrar una gramática no ambigua para el lenguaje de la segunda gramática de sección 4.1, lo podemos hacer por medio de dos operaciones: primero, aplicar orden de precedencia a los operadores y segundo aplicar asociatividad por la izquierda. Con esto, tenemos la sig. gramática:

  30. (cont.) SE$ nota: “$” es EOF- marker EE+T EE-T ET T-->T*F TT/F TF Fid Fnum F(E) Esta gramática acepta el mismo lenguaje que la de sección 4.1 pero solo produce un árbol de parsing por oración. Esta gramática no podría producir árboles como los siguientes: X + ?Y + ?U ?V * +

  31. 4.4 Analizador Sintáctico • En esta fase se analiza la estructura de la frase del programa. • El parser es el programa que funciona como núcleo del compilador. Alrededor del parser funcionan los demás programas como el scanner, el analizador semántico y el generador de código intermedio. De hecho se podría decir que el parser comienza el proceso de compilación y su primer tarea es pedir al escáner que envíe los tokens necesarios para llevar a cabo el análisis sintáctico, del estatuto, expresión o declaración dentro de un programa. • También el parser llama rutinas del análisis semántico para revisar si el significado del programa es el correcto. • Por ultimo el parser genera código intermedio para los estatutos y expresiones si no se encontraron errores en ellos.

  32. (cont.) Existen diferentes técnicas o métodos para realizar un análisis sintáctico “Parsing”. Estas técnicas se dividen en dos tipos: • Descendentes • Ascendentes Las técnicas descendentes realizan su análisis partiendo desde el símbolo inicial de la gramática y realizando derivaciones hasta llegar a producir las hojas o tokens. Por otra parte las técnicas ascendentes realizan su análisis partiendo de las hojas o tokens y mediante una serie de operaciones llamadas reducciones terminan en la raíz o símbolo inicial de la gramática. Por lo general las técnicas descendentes son mas sencillas de implementar que las ascendentes, pero por otra parte son menos eficientes

  33. 4.4.1 Analizador descendente (LL). Parsing Predictivo • Algunas gramáticas son sencillas de analizarse sintácticamente usando un algoritmo o método cuyo nombre es recursivo descendente. En esta técnica cada producción de la gramática se convierte en una cláusula de una función recursiva. • Un parser Predictivo es aquel que “ predice ” que camino tomar al estar realizando el análisis sintáctico. Esto quiere decir que el parser nunca regresara a tomar otra decisión ( back tracking ) para irse por otro camino, como sucede en las derivaciones. Para poder contar con un parser predictivo la gramática debe de tener ciertas características, entre ellas la mas importante es que el primer símbolo de la parte derecha de cada producción le proporcione suficiente información al parser para que este escoja cual producción usar. Normalmente el primer símbolo mencionado es un Terminal o token.

  34. (cont.) • Esta técnica se utilizó o popularizó en los años 70 a partir del primer compilador de pascal implementado con ella. A continuación ilustraremos esto escribiendo un parser recursivo descendente para la siguiente gramática: S  if E then S else S S begin S L S print E L end L ; S L E num = num Nuestro Parser tendría 3 métodos (uno por cada producción)

  35. Programa del Parser Final int if = 1, then = 2, else = 3, begin = 4, end = 5, print = 6, semi = 7, num = 8, EQ = 9 int tok = get token ( ); void advance ( ) { tok = get token ( ); } void eat ( int t) { if ( tok == 1) advance ( ); else error ( ); } void S ( ) { switch ( tok ) { case If: eat ( if ); E ( ); eat ( then ); S ( ); eat ( else ); S ( ); break; case begin: eat ( begin ); S ( ); L ( ); break; case print: eat ( print ); E ( ); break; default: error; }} void L ( ) { switch ( tok ) { case end: eat ( end ); break; case semi: eat ( semi ); S ( ); L ( ); break; default: error ( ); }} void E ( ) { eat ( num ); eat ( EQ ); eat ( num ); }

  36. (cont.) • Un parser predictivo que examina la entrada de izquierda a derecha (left-to-right) en un paso y realiza su derivación por la izquierda es llamado “Parser LL”. • Cuando el parser solo necesita “mirar” el siguiente token para hacer su función (llokahead(1)), recibe el nombre de Parser LL(1). • Un parser podría necesitar “mirar” K tokens por adelantado para tomar desiciones. En este caso recibe el nombre de parser LL(K). Ejemplo: (parte de una gramática) IF-STM  if EXP then STM else STM |  if EXP then STM

  37. Eliminación de Recursión por la izquierda • Suponer que queremos construír un Parser predictivo para la gramática de sección 4.3. SE$ EE+T EE-T ET T-->T*F TT/F TF Fid Fnum F(E) Producciones como E  E + T contienen recursión por la izquierda. Parsers descendentes no pueden manejar recursión por la izquierda en una gramática. Para eliminar este tipo de recursión utilizamos la siguiente transformación:

  38. (cont.) En general, si tenemos producciones X  X g y X  a, donde a no comience con X podemos aplicar la siguiente transformación: X  Xg1 X  a1X’ X  Xg2 X  a2X’ X  a1 X’  g1X’ X  a2 X’  g2X’ X’  l

  39. (cont.) • Aplicando la transformación a la gramática anterior, obtenemos la nueva equivalente gramática (sin recursión por la izquierda): S  E$ E  T E’ E’  + T E’ E’  - T E’ E’  l • T  F T’ T’  * F T’ T’  / F T’ T’  l • F  id F  num F  (E)

  40. Factorización por la izquierda • Otro problema en una gramática ocurre cuando dos producciones para el mismo no terminal comienza con el mismo símbolo. Por ejemplo: IF-STM  if EXP then STM else STM IF-STM  if EXP then STM En dicho caso podemos factorizar por la izquierda la gramática. El resultado sería el sig: IF-STM  if EXP then STM X X  l X  else IF-STM las producciones anteriores facilitarán el trabajo del parser predictivo.

  41. 4.4.2 Analizador ascendente(LR y LALR). • La debilidad de las técnicas descendentes LL(k) es que deben predecir que producciones usar, después de haber mirado los primeros k tokens de la cadena de entrada. • Una técnica ascendente mas poderosa es la técnica LR(K), la cual pospone la decisión hasta que ha visto los tokens de la entrada correspondientes a la parte derecha de la producción (además de k mas tokens también). • LR(K) quiere decir parser de izquierda a derecha, derivación por la derecha y “lookahead(k)”. Esta técnica fue introducida por primera vez en 1965 por Knuth.

  42. (cont.) • Un Parser LR(K) está compuesto de: • La cadena de entrada • Una pila • Una tabla de Parsing LR • El parser obtiene los tokens de la entrada y dependiendo de el token actual y de lo que está en el tope de la pila, ejecuta una de dos acciones (shift-reduce) con la ayuda de la tabla de parsing

  43. Algoritmo de Parsing LR (aho,Sethi y Ullman) Tomar el primer token de w$ /* w es la cadena */ Repeat forever begin Sea s el estado en el tope de la pila y a el token actual; if acción[s,a] = shift s’ then begin push a’ primero y sedpués s’ al tope de la pila; obten el siguiente token de la cadena de entrada else if acción[s,a] = reduce A->B then begin pop 2*|B| símbolos fuera de la pila; Sea s’ ahora el estado en el tope de la pila; Push A y después goto[s’,A] al tope de la pila; Imprime la producción A->B end else if acción[s,a] = accept then return else error() end

  44. id num print ; , + := ( ) $ S E L 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 S20 s10 g15 g14 r5 r5 r5 r5 r5 r2 r2 s16 r2 s3 s18 r3 r3 r3 s19 s13 r8 r8 S20 s10 s8 g17 r6 r6 s16 r6 r6 S20 s10 s8 g21 S20 s10 s8 g23 r4 r4 r4 r4 r4 s22 r7 r7 r7 r7 r7 r9 s16 r9 Ejemplo: Parser LR(K) TABLA DE PARSING S4 s7 g2 s3 a S4 s7 g5 s6 r1 r1 r1 S20 s10 s8 g11 s9 S4 s7 g12

  45. Ejemplo (cont.): PILA ENTRADA ACCION • a:=7;B:=c+(d:=5+6,d)$ shift 1 id4 := 7;B:=c+(d:=5+6,d)$ shift 1 id4 := 6 7;B:=c+(d:=5+6,d)$ shift 1 id4 := 6 num10 ;B:=c+(d:=5+6,d)$ reduce E->num • id4 := 6 E11 ;B:=c+(d:=5+6,d)$ reduce • S->id:=E . . . . . . . . . . . . . . . . . . . . . 1 S2 $ accept

  46. Parsing LALR • La técnica de parsing LALR (LookAhead-LR) evita el uso de tablas muy grandes, como las manejadas en la técnica de parsing LR. • Esta técnica fue inventada por DeRemer en 1971. • Casi cualquier construcción sináctica de un LP puede ser expresado de manera conveniente por una gramática LALR. • Generadores de Parsers famosos como YACC (Yet Another Compiler-Compiler-Johnson) producen un parser LALR. • Para una gramática de un LP como Pascal una tabla de parsing LALR ocuparía varios cientos de estados, mientras que una tabla LR serían miles de estados.

  47. 4.5 Administración de tablas de símbolos. • Como se estudió en la unidad anterior, durante el análisis de léxico se inicia la construcción de la tabla de símbolos. • Esto ocurre al detectarse las declaraciones en el programa fuente. • Sin embargo también el parser ayuda a realizar esta tarea pues el es quien llama a las respectivas rutinas semánticas para que realicen funciones relacionada con la tabla de símbolos (ver figura).

  48. 4.6 Manejo de errores sintácticos y surecuperación. • Dentro del código del parser predictivo estudiado en clase se puede observar que el parser llama un metodo “error” para el manejo de errores sintácticos, que es cuando el parser obtiene un token no esperado. • ¿Cómo se maneja el error para esta clase de parsers? Una forma simple es ejecutar una excepción y parar la compilación. Esto como que no es muy amigable par el usuario. • La mejor técnica es desplegar un mensaje de error y recuperarse de este, para que otros posibles errores sintácticos puedan ser encontrados en la misma compilación.

  49. (cont.) • Un error sintáctico ocurre cuando la cadena de tokens de entrada no es una oración en el lenguaje. La recuperación del error es una forma de encontrar alguna oración correcta, similar a la cadena de tokens. • Esto se puede realizar por medio de borrar, reemplazar o insertar tokens. • Por ejemplo la recuperación de un error en S, podría ser al insertar un token if,begin o print (o pretender que existe en la cadena), desplegar el mensaje del error y continuar la compilación.

  50. Ejemplo: void S ( ) { switch ( tok ) { case If: eat ( if ); E ( ); eat ( then ); S ( ); eat ( else ); S ( ); break; case begin: eat ( begin ); S ( ); L ( ); break; case print: eat ( print ); E ( ); break; default: print(“se esperaba if, begin o print”); }} Un problema que puede ocurrir al insertar un token faltante es que el programa caiga en un ciclo infinito, por eso a veces es preferible y mas seguro borrar el token, ya que el ciclo terminará cuando el EOF sea encontrado. Esta técnica trabaja muy cercanamente con la tabla de parsing (cuando el parser se implementa con tablas). En un parser del tipo LR o LALR, la tabla de parsing tiene 4 acciones: shift, reduce, accept y error (entrada nula). Cuando el parser encuentra una acción error, se para el proceso de análisis y se reporta la falla.

More Related