TEMA 1 LENGUAJES DE PROGRAMACIÓN Lenguaje Un lenguaje es un instrumento de comunicación. Para que exista una comunicación debe existir comprensión mutua Lenguaje de programación Un lenguaje de programación tiene como fin la comunicación entre el programador y la máquina, comunicación, que resulta difícil por ser elementos de diferente naturaleza. Para superar ésta dificultad se crearon los procesadores de lenguajes, que deben unir el salto semántico entre los distintos lenguajes, el lenguaje del programador y el de la máquina Definición de un lenguaje de programación (desde el punto de vista del usuario) Un lenguaje de programación es una notación que sirve para escribir algoritmos que puedan ser ejecutados en una máquina; una notación precisa, rigurosa y con la pretensión de ausencia de ambigüedades. La anterior definición aporta poca información para su análisis. Ejemplo: Program saludo ; begin writeln (“Bienvenidos a la asignatura de TLP”) end. Definición de un lenguaje de programación (desde el punto de vista del análisis) Un lenguaje de programación es aquél que tiene: un léxico, una sintaxis y una semántica. La anterior definición aporta cierta información para conocer su análisis, que veremos seguidamente. Program ejemplo; var a: integer; x: real; begin read ( a); x:= a*2 ; write (x) end . Un lenguaje tiene un conjunto de componentes elementales e indivisibles: variables, literales, palabras reservadas, símbolos separadores, símbolos aritméticos,..etc que pueden formar parte de un programa fuente escrito en ese lenguaje. Al conjunto de componentes básicos e indivisibles que se pueden usar en el lenguaje, se le llama léxico de ese lenguaje,{ Program, id, ´;´,…}. Los componentes léxicos que forman el léxico de un lenguaje, se pueden asociar para formar estructuras: una asignación, una expresión,…etc. estructuras que pueden formar parte de un programa fuente escrito en ese lenguaje. Ejemplo correcto de una estructura en Pascal: Program ejemplo; var a: integer; x: real; begin read ( a); x:= a*2 ; write (x) end . Al conjunto de estructuras que permite el lenguaje, se denomina sintaxis del lenguaje .Para definir la sintaxis de un lenguaje se utilizan las gramáticas. Una gramáticas es un conjunto finito de reglas a través de las cuales se pueden generar el conjunto(por lo general infinito) de estructuras que forman la sintaxis de un lenguaje. El conjunto de estructuras que se pueden utilizar en un lenguaje deben de cumplir una serie de restricciones y condiciones entre los elementos del lenguaje que la componen: la compatibilidad de tipos en la estructuras de asignación, expresiones,…. El conjunto de condiciones o restricciones que deben cumplir las estructuras se llama semántica estática Program ejemplo; var a: integer; x: real; begin read ( a); x:= a*2 ; write (x) end . En Pascal- una variable debe estar definida antes de usarse, debe existir una compatibilidad de tipos entre los objetos que intervienen en una estructura ( es un lenguaje fuertemente tipado),…. El conjunto de estructuras que se pueden utilizar en un lenguaje, determinan una serie de acciones a realizar, en una asignación: evaluar el valor la parte derecha del operador de la igualdad y asignárselo a la memoria definida por la variable de la parte izquierda, etc…. Las acciones que debe realizar la máquina para acometer el significado de las distintas estructuras se llama semántica dinámica. 1 - Ejemplo anterior: obtener un valor entero de la entrada y ponerlo en la variable a, - El valor que tiene la variable a multiplicado por la constante 2, y el resultado se deja en la variable x - Escribir el contenido de la variable x Cuanto mayor sea la potencia (significación) de las estructuras del lenguaje, mayor será la diferencia de la semántica del lenguaje de programación y el lenguaje de la máquina donde se quiere ejecutar. La forma de representar una sentencia switch del lenguaje C++, es bastante diferente y más simple que la representación en lenguaje máquina. Nivel de un lenguaje El nivel (semántico) de los lenguajes es una medida indicativa de la complejidad del significado de los programas que pueden escribirse con ellos: se refiere al nivel de complejidad de las estructuras (construcciones) del lenguaje; suelen considerarse dos categorías más o menos genéricas: - lenguajes de alto nivel: C++, Java, Ada, Pascal - lenguajes de bajo nivel: lenguajes ensambladores y máquinas de diferentes máquinas Lenguaje máquina – conjunto de instrucciones de una máquina particular que son interpretadas por el hardware o microprogramas de la propia máquina. El conjunto de instrucciones están escritos en notación binaria. Los lenguajes máquina, son los lenguajes de menor nivel semántico. Lenguaje ensamblador- es una versión simbólica del lenguaje máquina. En donde las operaciones, direcciones de memorias, registros.. se dan de forma simbólicas, con ciertas ayudas a la depuración. Lenguajes de alto nivel – Lenguajes que están por cima de máquina y en principio son independientes de la máquina sobre la que se van a ejecutar los programas escritos en la misma. La separación semántica Separación semántica es la diferencia de nivel semántico que se da entre un algoritmo escrito en un lenguaje de programación (de un cierto nivel) y las instrucciones máquina mediante las que ese algoritmo se ejecuta en una máquina (el nivel semántico mínimo para esa máquina). Para que un algoritmo escrito en un lenguaje (de determinado nivel) sea ejecutable en una máquina hay que resolver la separación semántica relativa a dicho lenguaje. Un ejemplo puede ilustrar fácilmente la diferencia: la semántica (el significado) de la sentencia cíclica for de Java es mucho más compleja (de significado más complicado, de mayor semántica, más costosa de ejecutar, más difícil de explicar) que la instrucción máquina que permite almacenar un valor constante en un registro de uso general de la unidad central de proceso. También puede hablarse de lenguajes de nivel intermedio. Hay lenguajes que tienen, a la vez, unas características que pueden considerarse de alto nivel y otras de nivel bajo o intermedio. lenguaje fuente (de cierto nivel) ▫ sentencias del lenguaje ▫ algoritmo [ significado común ] separación semántica □ nivel de la máquina (física) ▫ instrucciones ejecutables en la máquina ▫ Procesador de lenguajes Se puede definir un procesador de un lenguaje de programación (ya comentado anteriormente) como un sistema que resuelve la separación semántica de los programas escritos en ese lenguaje; un procesador está formado por uno o más programas interrelacionados que hacen posible que un algoritmo (programa) codificado (escrito) en el lenguaje pueda llegar a ejecutarse en una máquina (física). Tipos de procesadores de lenguajes Traductores, Intérpretes. 2 Traductor Un traductor entre dos lenguajes de programación, es un programa que realiza una transformación (una traducción) entre ellos: el lenguaje de entrada al traductor (lenguaje fuente) y el de salida del traductor (lenguaje destino); el traductor acepta un texto escrito en el lenguaje fuente (representativo de un determinado algoritmo/programa) y emite como salida una representación de ese mismo algoritmo/programa codificada en el lenguaje destino. El traductor está escrito en un lenguaje de implementación Li , o también llamado host. El proceso de traducción no altera el significado del programa traducido. El traductor analiza el programa de entrada verificando que está correctamente codificado según la definición del lenguaje fuente; si el programa analizado no tiene errores se puede traducir; en caso contrario, el traductor avisa de los errores detectados. La entrada al traductor se denomina programa fuente y la salida programa destino (o también código generado por el traductor). -- Traductor entre los lenguajes L y L’ P/L T [Li] P’/L’ M E P programa escrito en L (lenguaje fuente) P' programa escrito en L’ (lenguaje destino) E errores (si existen y son detectados) M máquina donde se ejecuta el traductor Li lenguaje de implementación de T [ P y P' representan idéntico algoritmo ] Lo más frecuente y razonable es que un proceso de traducción disminuya el nivel semántico: el nivel de lenguaje destino es menor que el nivel del lenguaje fuente. Tipos de traductores Compiladores, ensambladores, conversores,.. Compilador Un compilador, traduce un programa escrito en un lenguaje fuente de alto nivel a otro semánticamente equivalente escrito en un lenguaje objeto, lenguaje que puede ser máquina o de bajo nivel El resultado de la compilación es un programa ejecutable en una determinada máquina Puede afirmarse que la compilación directa a código máquina directamente ejecutable, no suele darse con frecuencia debido a que tras de una compilación suelen aparecer acciones (enlazador, cargador,..) para obtener el código ejecutable. La ejecución del programa compilado puede considerarse como un proceso de interpretación del procesador. La ejecución, es un proceso posterior en el que se ejecuta el programa objeto, teniendo un conjunto de datos de entrada y produciendo unos resultados como salida El siguiente esquema ilustra el proceso de ejecución de un programa compilado, con sus dos partes bien diferenciadas: la compilación del programa fuente (ejecución del compilador) y la ejecución del programa resultado de la compilación. -- compilador del lenguaje C M P[O] P[O] M' D R P[L] C compilador del lenguaje L, para la máquina M P [ L ] programa (fuente) escrito en L P [ O ] programa (código) objeto resultante de la traducción (compilación) 3 D datos para una ejecución del programa P R resultados de una ejecución del programa P Máquina anfitriona - máquina donde se ejecuta el compilador ( M ) Máquina destino- máquina para la que genera código el compilador ( M' ) El tiempo que se tarda en traducir un texto de un lenguaje se llama tiempo de compilación. El tiempo que tarda en ejecutarse el texto de un lenguaje se llama tiempo de ejecución. Un compilador es un programa que traduce un único lenguaje fuente, produciendo código para una única máquina destino. Fases de un compilador. Representaciones intermedias. La tarea realizada por un compilador es muy compleja y por ello conviene considerarla descompuesta en diferentes partes. Un enfoque tradicional (y bastante provechoso) en el análisis del problema de compilar un programa escrito en un lenguaje de alto nivel es el denominado modelo analítico-sintético. Se considera una descomposición funcional del compilador en diferentes fases. En una primera aproximación se diferencian dos fases: análisis y síntesis; en la fase de análisis se comprueba que el programa está bien codificado de acuerdo con la definición del lenguaje fuente y se extrae el significado del texto analizado; en la fase de síntesis se transcribe ese significado empleando la notación del lenguaje destino, generando el código máquina que representa el mismo algoritmo que el programa fuente. Para separar claramente estas dos fases conviene desde un punto de vista conceptual considerar que, como resultado de la fase de análisis se obtiene una representación intermedia del programa analizado que refleja las operaciones que el programa ha de realizar y el orden en que deben de llevarse a cabo; la naturaleza de esta representación intermedia depende del método empleado para la implementación del compilador P[F] Fase de análisis Representación intermedia P[I] Fase de Síntesis P[O] P[ F ] programa P escrito en el lenguaje fuente P[ O ] programa P escrito en el lenguaje destino P[ I ] representación intermedia del programa P La representación de lenguaje intermedio es el lenguaje de una máquina abstracta ( comentada mas adelante )usado como interfaz entre el análisis y la generación de código. La representación intermedia no es obligatoria en las fases de un compilador, puesto que un compilador puede generar código objeto directamente (compilador puro). Análisis: la fase de análisis debería ser independiente de la máquina, mientras que la de síntesis depende de la máquina en la que se ejecutará. Síntesis: A partir del código intermedio generado en la fase de análisis, generan y optimizan el código, dependiendo ahora de la máquina en la que se ejecutará En una segunda aproximación, la fase de análisis se considera, a su vez, descompuesta en tres fases cada una de ellas encargada de examinar las diferentes características del programa analizado: - el análisis lexicográfico encargado de comprobar las características lexicográficas (la forma en que están codificados los componentes elementales del texto), - el análisis sintáctico dedicado a verificar la estructura sintáctica de la secuencia de componentes léxicos que les pasa el análisis anterior, y que constituyen el programa, - el análisis semántico que realiza las comprobaciones de las restricciones de semántica estática que han de cumplirse para que el programa analizado tenga un significado correcto. En cualquiera de la fases de análisis pueden encontrarse errores: lexicográficos, sintácticos, semánticos; en el momento en que se encuentra un error se decide que ya no es necesaria la generación de código: el programa analizado es incorrecto. 4 Durante el análisis de un programa, el compilador recopila una gran cantidad de información que es necesario tener organizada y fácilmente accesible (para hacer las comprobaciones semánticas y para generar el código); la principal estructura de datos donde el compilador almacena la información que obtiene del programa analizado se denomina tabla de símbolos. En la fase de síntesis se realiza la tarea de generar el código destino; esta tarea puede hacerse directamente en un único paso o bien considerar que primero se obtiene el programa escrito en un lenguaje intermedio y después se transforma el lenguaje intermedio al código máquina. Para generar código se tiene en cuenta la semántica dinámica del lenguaje que es la que establece lo que la máquina ha de realizar para ejecutar lo indicado por el programa fuente. Fase de Análisis Código fuente Análisis léxico Componentes léxicos Análisis sintáctico árbol sintáctico Análisis semántico Árbol anotado Fase de Síntesis Generación de cód. intermedio Código intermedio Optimización de cód. intermedio Código intermedio Generación de código Código objeto Optimización de código Código objeto A veces también se considera como una fase dentro de la síntesis la tarea de optimización; un compilador puede efectuar intentos de mejora (optimización) en el código generado para que ocupe menos espacio o para que se ejecute con mayor rapidez; estas optimizaciones pueden hacerse en diferentes momentos: sobre la representación intermedia, sobre el código destino o sobre el lenguaje intermedio (si se ha producido la generación en dos etapas); precisamente uno de los motivos que justifica la generación del código en dos o más etapas es la posibilidad de hacer diferentes optimizaciones. Tipos de compiladores Compilador cruzado Se genera código en lenguaje objeto para una máquina diferente a la que se está utilizando para compilar. Uno de los problemas que se encuentra en un nueva máquina es que alguien tiene que escribir el primer compilador. La tarea será más cómoda de hacer en la máquina que se dispongan herramientas software, para después trasladarla a otra. Autocompilador Es un compilador escrito en el mismo lenguaje que compila. Cuando se extiende entre muchas máquinas diferentes el uso de un compilador, y éste se desea mejorar, el nuevo compilador se escribe con el antiguo, de manera que pueda ser compilado por todas esas máquinas diferentes, y dé como resultado un compilador más potente de ese mismo lenguaje. Metacompilador Es sinónimo de compilador de compiladores. Se refiere a un programa que recibe como entrada las especificaciones del lenguaje para el que se desea obtener un compilador y genera como salida el compilador para ese lenguaje Decompiladores Realizan la tarea inversa a la de los compiladores. Traduce un programa fuente en un lenguaje de bajo nivel a otro objeto de nivel superior. En la mayoría de los casos traducen un lenguaje máquina a un lenguaje ensamblador, también llamados desensambladores 5 Compilador incremental Es aquel que compila un programa en el que si después se descubren errores, en vez de corregir el programa fuente y compilarlo por completo, se compilan solo las modificaciones. Lo ideal es que solo se recompilen aquellas partes que contenían los errores, y que el código generado se reinserte con cuidado en el lenguaje objeto generado cuando se encontraron los errores. Sin embargo esto es muy difícil. Compilador con montador y cargador Compilador de distintos módulos de forma independiente y después los enlaza. Una de las características que diferencian a unos compiladores de otros es si tienen o no la posibilidad de traducir una parte de un programa de manera independiente y separada de la traducción de las otras partes que conforman el programa completo. En el siguiente esquema se muestra un programa completo que está dividido en n partes y cada una de ellas ha sido compilada en un proceso separado de compilación; de esta forma se han obtenido n trozos de programa traducido que no son todavía programas ejecutables; para formar un único programa ejecutable hay que unir las n partes obtenidas en los procesos de compilación. Sobre los trozos de programa resultantes de las compilaciones hay dos tareas pendientes de realizar: - la resolución de las referencias: es posible que en alguna de las partes separadas del programa se haga referencia (se use) una entidad que ha sido definida (declarada) en otra parte distinta, - la obtención de las direcciones definitivas para los componentes del programa considerado como un todo, una vez que se hayan juntado las distintas partes traducidas por separado. C P F [1] P O [1] C P F [2] ∙∙∙∙ P O [2] P O[reu] Montador (Cargador) Prog ecutable en memoria P O[eje] ∙∙∙∙ P F [n] C compilador P O [n] montaje ejecución Estas tareas las resuelve un programa auxiliar que se denomina montador o montador-cargador tal y como se ilustra en el anterior esquema. A la salida del montador se obtiene un programa completo que ya está preparado para su ejecución. Hay lenguajes para los que se dispone de una serie de librerías (trozos de programa) que realizan determinadas tareas (más o menos especializadas); en este caso el montador puede incorporar el código de esas librerías para formar un todo con el programa previamente compilado en el que se ha hecho uso de ellas. Pasadas de un compilador La descomposición en fases de un compilador se refiere al análisis del problema que se plantea al desarrollar un compilador para un lenguaje. Una vez analizado el problema y descompuesto en tareas más sencillas, hay que diseñar una manera de hacer la implementación. Desde el punto de vista de la implementación se fija la cantidad de pasadas que el compilador hace sobre el programa analizado; en cada pasada se hace un tratamiento del programa completo y se realiza sobre él alguna parte del trabajo de la compilación; el resultado de cada pasada es el punto de partida para la pasada siguiente, esto es, cada pasada se acerca más al resultado pretendido por el compilador. Pasadas – son el número de veces que se procesa una representación del programa fuente. Cada pasada requiere: lectura del código fuente, procesamiento y almacenamiento de la información obtenida. 6 Si todas las fases de la compilación se realizan examinando una única vez el texto de entrada, entonces se trata de un compilador de una única pasada. Si el lenguaje tiene una semántica compleja puede requerir que cualquier implementación de un compilador requiera más de una pasada. El método empleado para implementar el compilador también influye en el número de pasadas requeridas. Por ejemplo, los lenguajes que permiten que el uso de una entidad (variable, subprograma, método…) pueda preceder a su declaración (como ocurre en Java) requieren compiladores diseñados con más de una pasada. Las pasadas suelen agruparse: 1ª pasada – A léxico, A sintáctico, A. semántico y generación de código intermedio 2ª “ – Generación y optimización de código Un compilador con un número grande de pasadas hace que se necesite menos memoria para la ejecución, pero aumenta el tiempo de ejecución al realizar más operaciones de entrada salida, se facilita la detección de errores Compilador de una o varias pasadas Número de veces que hay que analizar el código fuente. Típicamente una pasada para realizar el análisis léxico y sintáctico, otra pasada para el análisis semántico y optimización dependiendo del lenguaje fuente y una tercera pasada para la generación de código y optimización dependiente de la máquina C y Pascal son lenguajes de una pasada mientras que el lenguaje modula realiza 2 pasadas Ensambladores Ensamblador Un traductor cuyo lenguaje fuente es un ensamblador y el lenguaje objeto es el lenguaje máquina. Existen ensambladores con macroinstrucciones: Macroensambladores. Desensambladores Caso inverso a los ensambladores, traducen de código máquina a ensamblador. Es un caso más fácil puesto que hay una correspondencia directa entre las instrucciones ensamblador y el código máquina. Conversores Traducen un lenguaje de alto nivel a otro lenguaje de alto nivel, para conseguir mayor portabilidad. Por ejemplo en un ordenador sólo hay un compilador de PASCAL, y queremos ejecutar un programa escrito en C; Un conversor C –> PASCAL nos solucionaría el problema Intérprete Un intérprete es un programa que analiza y ejecuta simultáneamente un programa escrito en un lenguaje fuente. Un intérprete hace una simulación de la ejecución del programa de entrada en la máquina donde se está ejecutando el intérprete. El intérprete va considerando una tras otra las instrucciones del programa interpretado y, para cada una de ellas, analiza su significado y procede de inmediato a realizar las tareas correspondientes, antes de pasar a la siguiente instrucción; en particular, si en el programa se indican operaciones de entrada/salida, el intérprete controlará su realización. -- Intérprete del lenguaje D (datos) I P/L M R (resultados) E 7 P D R E M programa escrito en L datos para una ejecución de P resultados de una ejecución de P errores (si existen y son detectados) máquina donde se ejecuta el intérprete Tipos de intérpretes: (según la estructura interna del intérprete) Intérpretes puros Los lenguajes no necesitan de pseudocompilación, analizan y ejecutan sentencia a sentencia todo el programa fuente, pueden considerarse intérpretes puros los intérpretes de comandos de la mayoría de los sistemas operativos. La interpretación pura en los lenguajes de alto nivel no suelen contruirse puesto que el proceso es bastante ineficaz. Intérpretes avanzados (o normales) Incorporan un paso previo de análisis de todo programa fuente a pseudocompilación generando posteriormente un lenguaje intermedio que es ejecutado por el mismo. Intérpretes incrementales Lenguajes que no se pueden compilar directamente, existen objetos que no son conocidos en tiempo de compilación ( Lisp, prolog, smaltalk) Comparación entre un traductor y un intérprete Aunque a simple vista pudiera apreciarse que las funciones de un traductor y de un intérprete son similares (ambos aceptan como entrada un programa escrito en un determinado lenguaje de programación y con ambos se pretende conseguir la ejecución de ese programa), sin embargo los conceptos de traducción y de interpretación son esencialmente distintos. Pueden enunciarse abundantes diferencias entre los procesos de traducción y de interpretación, entre las más significativas están: • una traducción siempre produce como salida una representación del programa analizado (el código destino); si ese código fuera ejecutable en alguna máquina; su ejecución habría de realizarse en otro momento (distinto del de la propia traducción) • una interpretación simula la ejecución de un programa; dicha ejecución es controlada por el intérprete; en una interpretación no se produce código destino En relación a un intérprete no existe el lenguaje destino (no se genera código); aunque el concepto de lenguaje fuente es más propio de los traductores, también puede decirse que el lenguaje para el que se construye el intérprete es su “lenguaje fuente”. Un intérprete necesita menos memoria, es más fácil de depurar, tiene una mayor flexibilidad a la hora de modificar las características del lenguaje fuente, tienen una mayor portabilidad, potencian el uso de los sistemas interactivos, la dificultad y coste de programación son menores( no necesitan fase de generación de código, optimización, montaje y reubicación). No son útiles con estructuras complejas, programas que trabajan en modo producción y la velocidad es importante e instrucciones son ejecutadas con frecuencia. En la actualidad suelen mezclarse ambas técnicas (apdo. de modalidades de separación semántica) : la primera agiliza la fase de producción debido al desarrollo de técnicas y herramientas de construcción de compiladores, mientras que la segunda facilita la ejecución y depuración Se puede decir que un ordenador es un intérprete de su propio lenguaje máquina, es decir siempre se produce un proceso de interpretación. Un intérprete puede relacionarse a nivel de software o hardware Lenguajes intermedios Para acercar la separación semántica de un programa escrito en un lenguaje de alto nivel a lenguaje máquina, puede emplearse una descomposición del problema apoyada en la utilización de un lenguaje intermedio. 8 Un lenguaje intermedio es una notación que permite representar algoritmos (secuencias de operaciones) y que recibe ese nombre por tener un nivel semántico medio situado entre el alto nivel del lenguaje fuente y el bajo nivel del lenguaje destino (máquina). Puede decirse que un lenguaje intermedio es una representación más abstracta y uniforme que un lenguaje máquina concreto. Con el uso de un lenguaje intermedio, el primer paso en la resolución de la separación semántica consiste en la traducción del programa escrito en el lenguaje fuente (alto nivel) al lenguaje intermedio; aunque el programa resultante de esa traducción todavía no es directamente ejecutable en una máquina, se ha conseguido disminuir la separación semántica entre el lenguaje fuente y la máquina destino. Leng. Fuente Separación semántica entre lenguaje fuente e intermedio Leng. Intermedio Separación semántica entre lenguaje intermedio y máquina Leng. máquina Los lenguajes intermedio no son lenguajes de programación de ninguna máquina real, sino que corresponde a una máquina abstracta. Diseño de los lenguajes intermedios: - Lenguajes intermedios de alto nivel - lenguajes diseñados para actuar en las primeras fases de traducción (análisis) o incluso de procesado. Son dependientes del lenguaje fuente e independiente de la arquitectura destino. Las tareas de dicho lenguajes – comprobación de tipos, generación de código y optimización de código con independencia de la plataforma. Las representaciones de lenguajes intermedios de alto nivel mas empleados son: árboles de sintaxis abstracta AST, grafos dirigidos acíclicos GADs y grafos de dependencia. Facilitan la traducción del lenguaje fuente; sus características están “próximas” al lenguaje fuente; por ejemplo el código P definido para favorecer la implementación del lenguaje Pascal, - Lenguajes de medio nivel válidos para representar un conjunto amplio de lenguajes fuente ( no siendo dependientes de uno concreto), válidos para representar un conjunto extenso de arquitecturas hardware. Representaciones mas empleadas: máquinas de pila (representación más empleada de código intermedio) , de tres direcciones(tercetos), de cuatro direcciones (cuartetos) y notación polaca inversa (recorrido post orden) - leguajes intermedios de bajo nivel – permiten traducir a distintos micros, de una misma arquitectura (dependientes de ésta), se basan en lenguajes con registros simbólicos Influidos por la máquina destino, están “próximos” al lenguaje máquina. Lenguaje1 Lenguaje2 Lenguajen Traducción directa n lenguajes n*m Plataforma1 Plataforma2 P Plataforma O [1] m m Plataformas Traducciones de n lenguajes a m plataformas m*n 9 En la anterior representación puede verse que para n lenguajes sobre m plataformas se necesitan n*m traducciones Ventajas del lenguaje intermedio Portabilidad de las aplicaciones: El código intermedio, al ser independiente de la plataforma puede ser ejecutado en cualquiera que tenga disponible una maquina virtual Adecuación de paradigmas complejos: Determinados paradigmas de programación son complicados de traducir a código binario dado su bajo nivel de abstracción, son traducidos a código intermedio Distribución de aplicaciones por internet: Los módulos de aplicaciones web se pueden distribuir a través de un servidor. Cualquier módulo de Internet dispone de una máquina virtual para cod. intermedio Intercomunicación de aplicaciones: La representación binaria de datos en distintas plataformas varía. El empleo de una misma maquina abstracta evita tener que traducirlos Inconvenientes Introduce en el compilador una nueva fase de traducción. Perdida de eficiencia no permite una compilación de una pasada Dificultad para definir un lenguaje intermedio adecuado en el compromiso entre lenguaje fuente y lenguaje máquina. Portabilidad Lenguaje1 C Lenguaje2 PASCAL Lenguajen JAVA n Lenguajes Compilación a máquina abstracta n+m Lenguaje intermedio Traducción a máquina real Plataforma1 Intel Plataforma2 Motorola Plataforman Dec-Alpha m Plataformas En la anterior representación puede verse, que con un lenguaje intermedio las traducciones se reducen de m*n a n+m El código intermedio de un compilador suele ser código del lenguaje de una máquina abstracta. Máquina diseñada sin la intención de ser implementada a nivel de hardware Máquinas virtuales Una máquina abstracta puede definirse como un modelo teórico para ejecutar un conjunto de instrucciones en algún lenguaje formal (no requiere implementación hardware, en tal caso se estaría hablando de una máquina concreta). Una máquina virtual es una máquina abstracta para la que existe un intérprete. También puede decirse que, una máquina virtual es una implementación software de la especificación de una máquina abstracta El lenguaje de la máquina virtual suele tener la función de lenguaje intermedio. Una máquina virtual se puede implementar sobre una máquina física; para ello ha de tenerse: - una estructura de datos que soporte la arquitectura de la máquina virtual, organización de memoria (pila, registros,…) - un intérprete del lenguaje de la máquina virtual que se ejecute en la máquina física.(conjunto de instrucciones que pueden ejecutarse. 10 Hay una gran variedad de máquinas virtuales: unas basadas en pila, otras en registros o una combinación de ambas; diseñadas para facilitar la implementación de lenguajes y para favorecer su portabilidad a distintas máquinas físicas; por ejemplo: - máquina virtual de java JVM (Java Virtual Machine) - máquina P para Pascal, - máquina A para Ada, - máquina EM, - ….. Los lenguajes intermedios facilitan la portabilidad de diferentes lenguajes de programación de alto nivel a distintas máquinas (físicas). Si se quiere implementar un lenguaje de alto nivel en diferentes máquinas, se puede desarrollar un único traductor del lenguaje fuente a un lenguaje intermedio y varios traductores (o intérpretes) del lenguaje intermedio a cada uno de los lenguajes máquina de las distintas máquinas (físicas). Por ejemplo, el lenguaje de la máquina virtual de Java (bytecode) facilita la implementación del lenguaje Java en diferentes máquinas físicas: se tiene un único traductor (compilador) de Java al lenguaje de bytecodes y una colección de intérpretes de bytecode (un intérprete por cada máquina física en la que se quieran ejecutar). El código bytecode se está convirtiendo en un código intermedio universal, ya existen intérpretes de JVM (máquina virtual de java) para todos los sistemas operativos. De ahí el famoso axioma que sigue a Java: "escríbelo una vez, ejecútalo en cualquier parte", o "Write once, run anywhere". Lenguaje1 Lenguaje2 PLenguajen O [1] N lenguajes Compilación a máquina abstracta N+m Lenguaje intermedio Traducción a máquina real Intérprete LI para P1 Plataforma1 Intérprete LI para P2 Plataforma2 P Intérprete O [1] LI para P2 Plataforman Máquinas Virtuales M Plataformas En lugar de traducir el código intermedio a código objeto de la plataforma destino, podemos utilizar una máquina virtual para cada plataforma El principal objetivo es reducir el número de programas necesarios para construir un traductor portable que permita generar código sobre una gran variedad de plataformas Arquitectura front-end/back-end Con frecuencia, las fases se agrupan en una etapa inicial (Front-End) y una etapa final (Back- End). La etapa inicial comprende aquellas fases, o partes de fases que dependen principalmente del lenguaje fuente y que son en gran parte independientes de la máquina objeto. Ahí normalmente se introducen los análisis léxicos y sintácticos, la creación de la tabla de símbolos, el análisis semántico y la generación de código intermedio. La etapa inicial también puede hacer cierta optimización de código e incluye además, el manejo de errores correspondiente a cada una de esas fases. La etapa final incluye aquellas partes del compilador que dependen de la máquina objeto y, en general, esas partes no dependen del lenguaje fuente, sino sólo del lenguaje intermedio. En la etapa final, se encuentran aspectos de la fase de optimización de código además de la generación de código, junto con el manejo de errores necesario y las operaciones con la tabla de símbolos. 11 Front - end Código fuente Back-end Análisis léxico Comp. léxicos Generación de código Análisis sintáctico Árbol sintáctico Arquitectura Front- end / Back-end Optimización de código Análisis semántico Análisis sintáctico Árbol anotado Código objeto Generación de cód. int. Código intermedio Optimización de cód. int Se ha convertido en rutina el tomar la etapa inicial de un compilador y rehacer su etapa final asociada para producir un compilador para el mismo lenguaje fuente en una máquina distinta. Otro motivo que justifica la división de la generación de código en dos etapas (intermedio y objeto) es la portabilidad: se puede construir una única parte frontal que sirva para traducir el lenguaje fuente a un determinado lenguaje intermedio y varias partes terminales que generen código traduciendo ese lenguaje intermedio a los lenguajes máquinas de distintas máquinas. Front- end Para el lenguaje C Lenguaje intermedio Back-end – Dec-Alpha Back-end – Motorola Back-end – Intel Cod. maq – Dec-Alpha Cod. maq – Motorola Cod. maq – Intel Compilador C para tres máquinas diferentes. También resulta tentador compilar varios lenguajes distintos en el mismo lenguaje intermedio y usar una etapa final común para las distintas etapas iniciales, obteniéndose así varios compiladores para una máquina. Pascal Java Front - End Front -End C Front End Back End Cod. maq – Intel Tres compiladores ( pascal, java y C) para una misma máquina 12 La representación intermedia actúa como medio de comunicación entre el front-end y back-end. - Si se cambia el lenguaje fuente, se reescribe el front-end. - Si se cambia la máquina objeto, entonces se reescribe el back-end - Si aparece una nueva arquitectura, basta con desarrollar un traductor del lenguaje intermedio a esa máquina Modalidades de procesadores. Rara vez se realiza el proceso de traducción o interpretación (pura) para resolver el problema de separación semántica, en la mayoría de los casos coexisten, dándose primero la traducción del fuente a una representación intermedia, a la cual se le aplica el proceso de interpretación. Todo lenguaje de programación necesita de uno o más procesadores de lenguajes. Cada máquina física requiere de un código específico lenguaje objeto (x 86) Para resolver el problema de la separación semántica pueden desarrollarse distintas modalidades de procesadores de lenguajes. * Un compilador Un copilador, por sí mismo constituye un procesador de un lenguaje de alto nivel; en el proceso de compilación se obtiene el código directamente ejecutable en la máquina ( Pascal, C, ADA). n1 Compilador n2 n1 n2 nivel de L (lenguaje de alto nivel) nivel de la máquina (física) * Dos traductores Un traductor del lenguaje fuente a un lenguaje intermedio y otro traductor del lenguaje intermedio al lenguaje máquina. n1 Traductor n2 n1 n2 n3 nivel de L (lenguaje de alto nivel) nivel del lenguaje intermedio nivel de la máquina (física) Traductor n3 * Un intérprete Aunque no es habitual su utilización (por cuestiones de eficacia), puede considerarse un proceso de interpretación directa (pura) de un lenguaje de alto nivel; en este caso el propio intérprete constituiría un procesador para el lenguaje de alto nivel. n1 Intérprete n2 n1 n2 nivel de L (lenguaje de alto nivel) nivel de la máquina (física) 13 * Un traductor y un intérprete Un traductor del lenguaje fuente a un lenguaje intermedio y un intérprete del lenguaje intermedio (java, Perl, PHP). n1 Traductor n2 n1 n2 n3 nivel de L (lenguaje de alto nivel) nivel del lenguaje intermedio nivel de la máquina (física) Intérprete n3 Herramientas para la construcción de procesadores de lenguajes A parte de las herramientas de desarrollo de software convencionales como: editores, depuradores, perfiladores,…, para la construcción de procesadores de lenguajes, existen otras herramientas especializadas y cada vez más, que ayudan a la construcción de procesadores, entre los que podemos destacar los siguientes: Generadores de analizadores léxicos Generación basada en el uso de expresiones regulares, generan automáticamente el código fuente para el análisis léxico a partir de una especificación de los tokens. El generador es un autómata finito. El generador de nuestro estudio es JavaCC que genera código en java. Generadores de analizadores sintácticos Construyen el código fuente del analizador sintáctico a partir de la especificación de la gramática del lenguaje fuente. El generador de nuestro estudio es JavaCC que genera código en java. Analizadores de gramáticas Dada una gramática especificada formalmente, verifican si es de un determinado tipo o no. Normalmente se utilizan para verificar las gramáticas LL(k) y LR(k). Máquinas de traducción dirigida por sintaxis Producen un conjunto de rutinas que recorren el árbol sintáctico y generan código intermedio. Asocian una o más traducciones a cada nodo sintáctico. Generadores automáticos de código Trabajan con un conjunto de reglas que permiten la traducción del código escrito en lenguaje intermedio al lenguaje objeto. Las reglas suelen remplazar instrucciones de código intermedio por patrones que contienen las instrucciones equivalentes de la máquina objeto. Analizadores de flujo Suministran la información necesaria para realizar las optimizaciones de código. Entorno de desarrollo integrado Conjunto de aplicaciones que permiten la escritura de los procesadores: editor, compilador, depurador, enlazador… Herramientas relacionadas con los procesadores de lenguajes Las técnicas empleadas en la construcción de traductores, compiladores e intérpretes pueden aplicarse en la construcción de otras herramientas: Editores sensibles al contexto Avisan al programador de posibles errores sintácticos cuando está escribiendo un programa fuente. Actualmente es muy común editores con sintaxis resaltada con colores, Conversores de formato Utilizan las técnicas de los traductores para convertir una descripción de ficheros en otra. 14 Preprocesadores Toman como entrada un conjunto de instrucciones y generan código en un lenguaje de alto o medio nivel. Formateadores de código fuente Toman como entrada un código fuente y obtienen a la salida el mismo mostrado de manera que se pueda seguir la estructura del programa. Generadores de código Permiten desarrollar aplicaciones a partir de unas especificaciones muy compactas, que pueden ser tratadas como un lenguaje de aplicación. Un caso particular son los generadores de pantallas. Verificación estática de programas Leen el código fuente y lo analizan para descubrir errores potenciales sin ejecutar dicho programa. Formateadores de texto Reciben como entrada un texto con indicaciones de cómo se desea la salida y generan dicho texto formateado en un fichero, o para una determinada impresora. Pueden estar especializados para fórmulas matemáticas, químicas, música, etc. Intérpretes de comandos de un sistema operativo Reciben órdenes del sistema operativo, las analizan y las ejecutan ( Ej.: COMMAND.COM de MSDOS). Construcción de entornos operativos Caso particular del anterior en el cual las órdenes suelen recibirse de forma gráfica ( Ej. WINDOWS). Intérpretes para consultar base de datos: reciben las consultas, las analizan y las ejecutan (EJ.: SQL). Compiladores de silicio Utilizan las mismas técnicas de construcción de compiladores e intérpretes pero implantadas en hardware. Procesamiento de lenguajes naturales Aplican las técnicas de construcción de traductores a los lenguajes naturales, permitiendo el análisis comprensión y traducción (inteligencia artificial). Reconocimiento del habla Se realiza un análisis de los sonidos para construir palabras (redes neuronales) Tipos de lenguajes de programación Los lenguajes de programación se pueden clasificar desde diferentes puntos de vista: Según el grado de independencia de la máquina (nivel): Lenguaje máquina Representa la forma más baja de un lenguaje de programación., escrita en la notación que entiende directamente un ordenador binario o hexadecimal. Se basa en la arquitectura de la máquina de Von Neumann. Lenguaje ensamblador Representa una versión simbólica de un lenguaje máquina. Cada código de operación se indica por un código simbólico: ADD, MUL...Asignaciones de memoria se dan con nombre simbólicos Lenguaje de nivel intermedio (caso del lenguaje C) Al ser un nivel intermedio dispone de características de los de bajo y alto nivel Características de los lenguajes máquina: Acceso directo a posiciones memoria. Almacenan variables en registros del procesador.. Características de lenguajes de alto nivel: Manejo de estructuras de control, Manejo de datos Lenguaje de alto nivel Características superiores a los lenguajes ensambladores No tienen acceso directo al sistema, estructuras de pueden ser datos complejas, utilización de bloques, procedimientos o subrutinas, (smalltalk, C++, Ada, Java, turbo Pascal…) Lenguajes orientados a problemas concretos Resolución de problemas en un campo específico (SQL, XBASE, PostScript, SPSS) 15 Según la forma de las instrucciones (características intrínsecas al lenguaje: Lenguajes imperativos o procedurales Orientados a instrucciones o sentencias, hacen uso masivo de variables, incorporación de mecanismos de bloques. Están Influidos por la máquina de Von Neumann. Programación estructurada ( Pascal, C++, COBOL..). Lenguajes declarativos: lógicos y funcionales Lenguajes de muy alto nivel con notación muy próxima al problema real del algoritmo que resuelven. - Funcionales: sus construcciones son llamadas a funciones. No hay instrucciones. Todo el programa es una función y todas las operaciones son funciones más simples. En la ejecución se aplica la función a datos de entrada (argumentos) y se obtiene el resultado calculado por la función (LISP). - Lógicos: instrucciones se forman siguiendo un tipo de lógica, maneja relaciones (predicados) entre objetos (datos). La relaciones se especifican con reglas y hechos. La ejecución consiste en demostraciones de hechos sobre las relaciones mediante preguntas (PROLOG). Lenguajes concurrentes: Dos o más tareas simultaneas o paralelas. Pueden ser una característica propia del lenguaje o el resultado de ampliar las instrucciones de un lenguaje no concurrente: Ada, Concurrent C, concurrent Pascal. Lenguajes orientados a objetos: Basados en la teoría de los objetos que permitiendo: la encapsulación, herencia y polimorfismo ( java, C++, smalltalk…). Según la generación: Primera generación: Lenguajes máquina y ensamblador en los años 50 Segunda generación: Lenguajes con asignación estática de memoria ( en tiempo de compilación). No manejan recursividad ni estructuras dinámicas de datos en los años 60. (FORTRAN, COBOL). Tercera generación: Programación estructurada en los años 70. Uso de subprogramas o módulos, variables locales, recursividad y estructuras dinámicas.(Algol, PL/I, PASCAL, MODULA). Cuarta generación: De muy alto nivel para tareas específicas a primero de los años 80. Base de datos, herramientas CASE Quinta generación: Inteligencia artificial (LISP PROLOG) Generación orientada a objetos: Con la proliferación de las interfaces gráficas de usuarios en los años 90 Generación visual: Exigencia de los usuarios de interfaces amigables en los años 90 (Visual Basic, Delphi) Generación Internet: Necesidad de manejar aplicaciones en diferentes plataformas dentro de internet (Java, XMN, HTML, VRML) Otros lenguajes En informática también se utilizan otros lenguajes que no son de programación y que tienen otras aplicaciones, como pueden ser para describir formatos de texto, gráficos, de sonido, etc. En cualquier caso, todos los lenguajes no naturales son formales, surgen primero las reglas gramaticales y se ajustan con todo rigor a ellas. Gracias a ello se pueden construir procesadores ( término genérico que hace referencia al proceso de transformación) Tipos de lenguajes que no son de programación: De descripción de páginas: Postcript, True Page.... De formatos gráficos no vectoriales: TIFF, GIF, PCX, BMP, JPEG.... De formatos gráficos vectoriales: DXF, CGM.... De formatos de bases de datos: DBF, DBT, MDX.... De formatos de texto: RTF, ASCII, Word, WordPerfect.... De formatos de archivos de sonido: WAV, MIDI, MP3.... De formatos de archivos de vídeo: AVI, MOV,... De formatos de ayuda: HLP de Windows, HLP de Turbo Visión... De gestión electrónica de documentos e hipertexto: pdf de Acrobat, HTML, XML..... 16 TEMA 2 LENGUAJES FORMALES 2.1.- Conceptos básicos sobre palabras. Lenguaje formal - Un lenguaje formal es un conjunto de palabras definidas sobre un alfabeto. Alfabeto - Se llama alfabeto a un conjunto finito, no vacío de símbolos. Se representan por (Σ). Un alfabeto se define por la enumeración de los símbolos que lo contiene. Ejemplos de alfabetos: Σ1 ={a,b}, Σ2={0,1}, Σmorse ={.- , .-- ,..}, ΣASCII ={A, %, …} ΣHTML ={<HTML>, <BODY>, A..Z, a ..z,.}, Σ=Conjunto de palabras de un diccionario Si Σ1 y Σ2 son alfabetos, Σ1 ∪ Σ2 es un nuevo alfabeto. Si Σ1 y Σ2 son alfabetos, Σ1 ∩ Σ2 es un nuevo alfabeto, siempre que Σ1 ∩ Σ2 ≠φ Símbolo - Un símbolo es un componente mínimo e indivisible que puede formar parte de una palabra. Un símbolo es una entidad abstracta que no tiene definición. Los símbolos pueden estar formados por más de un componente. a ∈ Σ1,, −.− ∈ Σmorse, <HTML>∈ΣΗΤΜΛ Cadena, palabra - Una palabra es a una secuencia finita de símbolos de un alfabeto. Las palabras se representan por los últimos símbolos del alfabeto castellano en minúsculas x.y,z,.. x(∑1)=abab ∀ a ∈ Σ, a es una palabra definida sobre Σ . Cualquier programa (p.e en Pascal) será una cadena sobre el alfabeto de caracteres ASCII Longitud de una palabra x - La longitud de una palabra es el conjunto de símbolos que tiene esa palabra, se representa por |x| x =aaaabb ; |x| = 6 Palabra vacía - es una palabra que no tiene símbolos, se representa por ε (épsilon), y su longitud es 0, x=ε ; |x|=0. Un alfabeto Σ, nunca tiene el símbolo vacío ε. Σ = {a, b}, Σ*={ε, a, b, ab, ba, aa, bb, aaa........}, Σ ⊆ Σ* Sea el alfabeto Σ, Σ* es el conjunto de todas las palabras que se pueden formar con Σ. Sobre cualquier alfabeto Σ , Σ* es infinito. Σ* se denomina lenguaje universal definido sobre Σ. Subpalabra - Sea una palabra x definida sobre Σ; subpalabra de x es una nueva palabra y, definida sobre el mismo alfabeto, formada por símbolos de x entrescados de forma consecutiva de ésta. Sean x, y, z, v ∈ Σ*, y es subpalabra de x si ∃ z, v ∈ Σ* | x = v·y·z Sea x=aab, donde Σ={a, b}, subpalabras de (x) = {ε, a, aa, b, ab, aab} Subpalabra propia - x es una subpalabra propia de y ⇔ existen z,v siendo alguna de ellas no vacía tales que vxz=y Prefijo de una palabra, es una subpalabra entresacada desde el inicio de dicha palabra. Sean x, y, z ∈ Σ*, y es prefijo de x si ∃ z ∈ Σ* | x = y·z Sea x=aab, donde Σ={a, b} , prefijos(x) = {ε, a, aa, aab} Prefijo propio - y es prefijo propio de x si x =y · z, si ∃ z ∈ Σ+ , z∉ε | x = y·z Sufijo de una palabra, es una subpalabra entresacada desde el final de dicha palabra. Sean x, y, z∈ Σ*, y es sufijo de x si ∃ z ∈ Σ* | x =z · y Sea x=aab, donde Σ={a, b}, sufijo de (x) = {ε, b, ab, aab} Sufijo propio - y es sufijo propio de x si ∃ z ∈ Σ+ , z∉ε | x = z.y Cualquier palabra x incluyendo la palabra vacía, son prefijo y sufijo de sí misma Subsecuencia de una palabra x, es una combinación de símbolos de dicha palabra en orden, pero no tienen que ser consecutivos. Sea x=abab, donde Σ={a, b}, subsecuencias de (x) = {ε, bb, ab,…..} 17 2.2.- Operaciones con palabras. Al estar los lenguajes formales compuestos de palabras, las operaciones de lenguajes formales, serán operaciones con palabras. Las operaciones a destacar con palabras son las siguientes: Concatenación de palabras (.) Sean dos palabras x,y definidas sobre el mismo alfabeto Σ, la concatenación de ambas x.y (xy) es una nueva palabra z, formada por los símbolos x seguidos de los símbolos de y. x(∑).y(∑)=x(∑)y(∑)=z (∑) x=ab, y=aa ; xy=z=abaa ; |x|+|y|=|z| Propiedades de la concatenación ∀ x, y ∈ Σ*, la operación concatenación (.) cumple las siguientes propiedades: Operación cerrada x.y =z P. asociativa x(yz) = (xy)z E. neutro xε = εx=x Potencia de una palabra Dado que la concatenación permite la propiedad asociativa, una palabra x, se puede concatenar consigo misma un número determinado n de veces, dando lugar a la potencia n-ésima de esa palabra xn x=aab, x0=ε , x3=aabaabaab Propiedades de la potencia ∀ x ∈ Σ* ⇒ ∃ xn∈ Σ* xn = ε si n=0 xn-1x si n>0 |xn|=n |x| Palabra inversa (refleja, transpuesta) Dada una palabra x definida sobre un alfabeto Σ, su inversa o refleja x-1 (xi ), es una nueva palabra definida sobre el mismo alfabeto, formada por los mismos símbolos que tiene x pero situados en orden inverso. x=aab , x-1=baa Propiedades de la inversión ∀ x ∈ Σ* ⇒ ∃ x-1∈ Σ* x-1 = ε si x= ε -1 y a si x=ay -1 |x|=|x | La operación inversa cumple la propiedad de idempotencia ( x-1 ) -1 = x Una palabra es igual a otra sí tienen la misma longitud y los símbolos en la misma posición. 2.3.- Conceptos básicos de lenguajes formales Un lenguaje formal L definido sobre un alfabeto ∑, es cualquier subconjunto de ∑*, conjunto de palabras con una significación propia (lógica común) y, tiene una definición formalizada L(∑) ⊆ ∑* L(∑)={x∈∑* | x cumple con la definición formal del lenguaje} 18 Ejemplos de lenguajes formales definidos sobre el alfabetos Σ = {a,b}: L1 = {}= ∅, lenguaje vacío Cardinal ({})=0 L2 = { ε }, lenguaje no vacío Cardinal ({ε })=1 L3 = {a, b}, el lenguaje coincide con el alfabeto L4 = {a, ab, ε} el lenguaje contiene la palabra vacía L5 = {an, n≥0}= {a0, a1, a2,…}={ ε, a, aa,…} lenguaje formado por palabras que contienen cero o más aes , definido por una notación matemática L6 ={anbm | n, m≥0}={a0b0, a0b1,… .}= {εε, εb,…}={ ε, b,…} lenguaje formado por palabras formadas por (0 o más) aes seguidas de (0 o más) bes, definido por una notación matemática L7 ={palabras formadas por un número impar de aes}={ a, ba, ab, bab, aaba,…} Σ*=lenguaje universal de Σ, o cierre de Σ . Lenguaje, formado de todas las palabras que se pueden construir con los símbolos {a,b} sin limitación de orden y longitud, incluyendo la palabra vacía. 2.4.- Operaciones con lenguajes. Al ser un lenguaje formal un conjunto de palabras, se puede aplicar toda la teoría de conjuntos sobre ellos. En éste texto vamos a estudiar las siguientes operaciones con lenguajes formales: Sean L1 y L2, lenguajes definidos sobre Σ, L1, L2 ⊆ Σ* L1 ={ε, a, aa, aab}, L2 ={b, bb, aa, aabb} - Alternativa o unión de lenguajes Dado dos lenguajes L1 y L2 definidos sobre un mismo alfabeto Σ, la unión de ambos es un nuevo lenguaje L3 , definido sobre el mismo alfabeto Σ y formado por las palabras de L1 y L2 L1 | L2 = L1 ∪L2 = {x∈ Σ * | x∈L1 ∨ x∈L2 } L1 ={ε, a, aa, aab} , L2 ={b, bb, aa, aabb} L1 | L2 ={ ε, a, aa, aab, b, bb, aabb} Propiedades que cumple la operación (|) unión de lenguajes ∀ L ⊆ Σ* se cumplen las siguientes propiedades: Operación cerrada P. idempotente P. conmutativa P. asociativa E. neutro L 1 | L2 = L3 L1| L1 = L1 L1| L2 = L2|L1 L1| (L2|L3) = (L1|L2)|L3 L1|φ = L1 - Concatenación o yuxtaposición Dado dos lenguajes L1 y L2 definidos sobre un mismo alfabeto Σ, la concatenación de ambos es un nuevo lenguaje L3, definido sobre el mismo alfabeto Σ y formado por las palabras de L1 concatenadas con las palabras de L2 L1L2=L1· L2 = { xy ∈ Σ*| x∈L1 ∧ y∈L2 } L1 ={ε, a, aa, aab} , L2 ={b, bb, aa, aabb} L1. L2 ={ εb, εbb, εaa, εaabb, ab, abb, aaa, aaabb, aabb, aabbb, aabaa, aabaabb } Propiedades que cumple la operación (.) concatenación de lenguajes ∀ L ⊆ Σ* se cumplen las siguientes propiedades: Operación cerrada L1 L2 = L3 P. asociativa L1 (L2L3) = (L1L2)L3 E. neutro L1.ε = ε.L1=L1 19 La operación concatenación de lenguajes formales cumple con la propiedad distributiva con respecto a la operación unión L1 (L2|L3) = (L1 L2|( L1L3) (L1|L2)L3 = (L1 L3)| ( L2L3) P. distributiva - Potencia de lenguajes Dado que la concatenación de lenguajes formales permite la propiedad asociativa, un lenguaje formal L1 se puede concatenar consigo mismo, un número determinado n de veces, dando lugar a la potencia n-ésima de ese lenguaje Ln Ln=LLL..n veces L L1 ={ε,a,aa,aab} L1L1=L12 ={εε, εa, εaa , εaab, aε, aa, aaa, aaab, aaε, aaa, aaaa, aaaab, aabε, aaba, aabaa, aabaab} Propiedades de la potencia de lenguajes ∀ L ⊆ Σ* ∃ Ln ⊆ Σ* L⊆ Σ * , Ln⊆ Σ * Operación cerrada Li .L = L.Li=Li+1 Cualquier lenguaje L0= ε ε Ln = si n=0 n-1 L L si n>0 En la definición anterior existe una incongruencia para el lenguaje L= φ , φ0= ε - L* (L estrella, cierre de Kleene) L* es el conjunto de palabras que se pueden formar al concatenar en cualquier orden y cantidad las palabras de L, incluyendo la palabra vacía. ∞ 0 1 n L*=L |L | …..L …= U Li i=0 Sea L1 ={ε, a, aa, aab} L1* ={ ε } | {ε,a,aa,aab} | {εε, εa, εaa , εaab, aε, aa, aaa, aaab, aaε, aaa, aaaa, aaaab, aabε, aaba, aabaa, aabaab} | {….} |… - L+ (L mas , cierre positivo de L) L+ es el conjunto de palabras que se pueden formar al concatenar en cualquier orden y cantidad las palabras de L ∞ L+=L1| L2…..Ln…=L+= U Li Sea L1 ={ε, a, aa, aab} i=1 L1+ ={ε,a,aa,aab} | {εε, εa, εaa , εaab, aε, aa, aaa, aaab, aaε, aaa, aaaa, aaaab, aabε, aaba, aabaa, aabaab} | {….} L*= L+| L0 ; L*= L+| {ε} ; L+= L* sii ε ∈ L ; L+=L*.L - Σ * ( sigma estrella, lenguaje universal sobre Σ) Σ* es el conjunto de palabras que se pueden formar al concatenar en cualquier orden y cantidad los símbolos de Σ , incluyendo la palabra vacía. ∞ Σ*= Σ |Σ |Σ ….= U Σ i 0 1 2 i=0 Σ*=L* sii Σ⊆L ; Σ*=L+ sii Σ⊆L y ε∈L 20 - Σ + (sigma mas) Σ+ es el conjunto de palabras que se pueden formar al concatenar en cualquier orden y cantidad los símbolos de Σ , ∞ Σ+= Σ1|Σ2….= U Σ i i=0 Σ =L sii Σ⊆L y ε∉L ; Σ ≠L* ; Σ +≠ Σ* + + + - Intersección de lenguajes Dado dos lenguajes L1 y L2 definidos sobre un mismo alfabeto Σ , la intersección de ambos L1∩L2 es un nuevo lenguaje L3 definido sobre el mismo alfabeto Σ y formado por las palabras que pertenecen a L1 y a L2 L1 ∩L2 = {x∈ ∑ * | x∈L1 ∧ x∈L2 } L1 ={ε, a, aa, aab} , L2 ={b, bb, aa, aabb} L1∩L2 ={aa} Propiedades de la operación (.) intersección de lenguajes ∀ L ⊆ Σ* se cumplen las siguientes propiedades: Operación cerrada L1∩ L2 = L3 P. idempotente L1∩ L1 = L1 P. conmutativa L1∩ L2 = L2∩L1 P. asociativa L1∩ (L2∩L3) = (L1∩L2) ∩L3 E. neutro L1∩ Σ *= L1 P. distributiva L1∪ (L2∩L3) = (L1∪ L2) ∩(L1∪ L3) L1∩ (L2∪L3) = (L1∩L2) ∪ (L1∩ L3) - Sublenguaje de otro Sean dos lenguajes definidos sobre un alfabeto: L1 ={ε, a, aa, aab}, L2 ={ ε, a, aa, aab, b, bb, aabb} L1 es sublenguaje de L2 si L1 ⊆L 2 Todo lenguaje L1 definido por un alfabeto Σ, es un sublenguaje del lenguaje universal Σ *, L1⊆ Σ * L1={ε, a, aa, aab} ⊆ { ε, a, b, aa, bb, ab, ba, aab,…..} Dos lenguajes son iguales si todos sus elementos son iguales - Diferencia de lenguajes formales Dado dos lenguajes L1 y L2 definidos sobre un mismo alfabeto Σ, la diferencia de ambos L1-L2, es un nuevo lenguaje L3 definido sobre el mismo alfabeto Σ y formado por las palabras que pertenecen a L1 y no pertenecen a L2 L1-L2 = {x∈ ∑ * | x∈L1 ∧ x∉L2 } L1 ={ε, a, aa, aab} , L2 ={b, bb, aa, aabb} L1-L2={ε, a, aab} Propiedades de la operación (-) diferencia de lenguajes ∀ L ⊆ Σ* se cumplen las siguientes propiedades: Operación cerrada L1- L2 = L3 P. idempotente L1 - L1 = L1 E. neutro L1- φ= L1 21 - Complemento de un lenguaje: Dado un lenguaje L definido sobre un alfabeto Σ, su complementario L, es un nuevo lenguaje formal definido sobre el mismo alfabeto, compuesto de todas las palabras que se pueden formar con los símbolos de Σ (∑* lenguaje universal) menos las palabras del propio L, L= ∑* - L __ L = { x∈ ∑ * | x∈ ∑ * ∧ x∉L} Propiedades: ∀ L ⊆ Σ* se cumplen las siguientes propiedades: Operación cerrada L ⊆ Σ* ; L ⊆ Σ* L = ∑* - L L =L L ∩L=φ L U L = ∑* - Otras propiedades: leyes de Morgan L1∩ L2 = L1 U L2 L1 U L2 = L1 ∩ L2 - Lenguaje inverso Dado un lenguaje L definido sobre el alfabeto Σ, su inverso L-1 es un nuevo lenguaje definido sobre el mismo alfabeto Σ y, formado por las palabras inversas de L L-1 = { x-1 ∈ Σ * | x∈L } L ={ε, a, aba, aab} , L-1 ={ ε, a, aba, baa } 2.5.- Mecanismos formales Un lenguaje formal se define por las propiedades que cumplen las palabras que lo componen. Las propiedades que cumplen las palabras del lenguaje deben permitir representarlas de una manera formalizada. Si el lenguaje es finito, para su definición basta con su enumeración, conocer su representación L= { a, aa, aaa} – lenguaje cuyas palabras están formadas por: una, dos o tres aes Si es infinito no puede ser enumerado, habrá que buscar un medio finito y preciso para su definición. Pero ocurre que no todos los lenguajes formales pueden definirse de una forma finita y precisa L={ε, a, ,aa, ..ab, aab,…b, bb,… abb…. .} = L={anbm | n,m≥0} Tiene una definición en notación matemática, pero dicha definición no aporta información para su tratamiento L= { a, abbaa, aabab, ababa, abbaa, aaabb,…aaaaabbbb,…….} {palabras que tienen un número impar de aes, empiezan por el símbolo a y tienen una a más que bes} La definición no es precisa, con las palabras anteriormente representadas, y dicha definición no aporta información para su tratamiento Para evitar los anteriores problemas de indefinición y tratamiento, se crearon los mecanismos formales: expresiones regulares, autómatas y gramáticas: Las expresiones regulares son mecanismos que sirven para describir un tipo de lenguaje formal (lenguajes regulares). Los autómatas son mecanismos que permiten especificar de manera finita y precisa cualquier lenguaje formal, mecanismos que permiten simular el reconocimiento del lenguaje para dicha especificación 22 Las gramáticas son mecanismos que permiten especificar de manera finita y precisa cualquier lenguaje formal, mecanismos permiten simular la generación de las palabras del lenguaje especificado. En este tema se ha hecho referencia a la definición lenguaje formal para diferenciarlo de lenguaje natural. En general, un lenguaje natural es aquel que ha evolucionado con el paso del tiempo para fines de la comunicación humana, evolucionan sin tener en cuenta reglas gramaticales estrictas que puede resolver situaciones ambiguas. Mientras que los lenguajes formales, al contrario que los naturales, están definidos por reglas de producción preestablecidas y se ajustan con todo rigor y formalidad a ellas. Ejercicios sobre lenguajes formales 1 - Alfabeto de entrada para los lenguajes- java , C, Pascal 2 - En una palabra de longitud n – cuantas subpalabras hay en las definiciones: prefijos, sufijos, prefijos propios,.. 3- Demostrar que (xy)-1= y-1.x-1 ∀ x,y ∈Σ*. 4 - Dados los siguientes lenguajes formales definidos sobre el alfabeto ∑= {a, b}: • • • • • • • • L0={} L1={ε} L2={a} L3={a,b} L4={(ab)n | 0<= n < 2 } L5={ambl | m,l >= 0 } L6={anbn | n>=0 } L7 ={lenguaje formado por las palabras que tienen un número impar de aes} Determinar para cada uno de ellos L0, L3 y L* 5- Sean las siguientes operaciones con los anteriores lenguajes formales: L0n L1n, L0n| L1n, L3n L1n, L3n | L1n, L3n L0n, L3n| L0n, L7n ∩ L5n, L7n| L5n , L0n , L3n, , L7n 1. Determinar qué lenguajes definen suponiendo que n = 0. 2. Determinar qué lenguajes definen suponiendo que n = 3 3. Determinar qué lenguajes definen suponiendo que n = * 6- Sea ∑ un alfabeto cualquiera y L un lenguaje cualquiera definido sobre el alfabeto anterior. Para cada una de las siguientes igualdades, justificar si siempre son ciertas o no. L*=L+ ∑*=L+ L =∑*- L = L*-L ∑+=L+ L+=L*-ε ∑+=∑*- ε ∑*=(∑*)-1 Φ *= ε=Φ+ L1-L2 = L1 ∩ L2 = L1∪L2 (L*)*=(L+)*=(L*)+=L*=(L+)+ (L*)I=(LI)* L1- (∑*-L1)=L1 L1-∑*-L1= Φ L1-L2 = L1 ∩ L2 = L1∪L2 7 - Sea L1 un lenguaje definido sobre un alfabeto Σ ={a,b} formado por las palabras que tienen un número impar de símbolos a y como máximo dos símbolos b consecutivos. Ejemplos válidos: a, aaba, aaabbaba, babbabbab Ejemplos no válidos: ε, aabbba, bb, abbabbaba Sea L2 un lenguaje definido sobre un alfabeto Σ ={a,b} formado por las palabras que tienen un número par de símbolos a (el 0 se considera un número par) y como máximo dos símbolos b consecutivos. Ejemplos válidos: ε, aa, aaaba, aaabbaaba, baabbabbab, bb Ejemplos no válidos: a, aabbbaa, aabbabbaba Justificar si se cumplen o no las siguientes igualdades: L1 | L2=Σ* , L1.L2=L2.L1 , L1*=Σ* , L1=L2 complementario 23 2.6.- Gramáticas Formales Una gramática es un mecanismo formal generador de lenguajes formales, que especifica de una manera finita y precisa el conjunto de palabras que componen el lenguaje al que se quiere definir. Una gramática está formada por un conjunto finito no vacío de reglas (producciones) a través de las cuales se obtiene (genera) un lenguaje. - Definición formal Una gramática es un mecanismo formal compuesto por cuatro elementos G = (Σ, N, S, P) en donde: Σ - es el alfabeto terminal, alfabeto de las palabras del lenguaje que genera la gramática N - es el alfabeto no terminal, alfabeto auxiliar que determina la complejidad de la estructura de las palabras a generar por la gramática Σ ∩ N=Φ S - es el símbolo inicial a partir del cual se obtienen todas las palabras del lenguaje a generar por la gramática S∈N P - es un conjunto finito de reglas de producción a través de las cuales se obtienen todas las palabras del lenguaje que genera la gramática. P: { α1→β1, α2→β2…..} , αi ∈ ( N | Σ)* N ( N | Σ)*, βi ∈ ( N | Σ)* Ejemplo Sea un lenguaje formal compuesto por las palabras que se ajustan a la siguiente definición: L(Σ )={anbm | n, m≥0}={a0b0, a0b1, a1b0, a1b1,…. Una gramática que genera dicho lenguaje es la siguiente: G = (Σ ={a,b}, N={S,A,B},S, P) S → AB A → Aa |ε B → Bb |ε - Derivación en el entorno de una gramática Derivación en el entorno de una gramática - es un proceso enfocado en la obtención de las palabras del lenguaje que genera dicha gramática P: Sea una gramática G = (Σ, N, S, P) P: { α1→β1, α2→β2, .. } α ∈ (N | Σ)*N( N| Σ)*, β ∈ ( N|Σ)* L(G)={ x∈ Σ* | xδG ( x deriva de G) } - Derivación directa (derivación de un paso) Consiste en aplicar sobre una palabra ω ∈ ( N | Σ)* N ( N | Σ)* obtenida de la gramática G, una producción α1→β1 de la propia gramática G. Sea la palabra ω1 = δ1α1δ2 ; δ1,δ2∈ ( N | Σ)* y α1 ∈ (N | Σ)* N (N | Σ)* Aplicando la producción α1→β1 de la gramática G sobre dicha palabra ω1, la palabra ω1 se transforma en δ1β1δ2= ω2 en donde δ1,β1,δ2∈ ( N | Σ)* Es decir δ1α1δ2→δ1β1δ2 , ω1→ω2 Por lo que puede decirse que la palabra ω1 produce la palabra ω2 (de ahí el nombre de producción en la definición de gramáticas), o bien que la palabra ω2 deriva de la palabra ω1. En un proceso de derivación de un paso o producción directa, es un proceso no determinista que sólo se aplica a una producción, aunque dicha producción u otras de la gramática se puedan aplicar sobre la misma palabra ω. Una palabra se deriva o se produce así misma sin aplicar el proceso de derivación, se dice que se deriva en cero pasos. 24 Relación de derivación Sean las palabras ωi,…ωm ∈ (N | Σ)* N (N | Σ)* se dice que están en relación de derivación en la gramática G, si la palabra (ωi produce la palabra ωm) o ωm deriva de la palabra ωi en cero o más derivaciones directas. ωi→ωj ; ωj→ωk ; ωk→ωm La representación de un proceso de cero o más derivaciones se representa de la siguiente forma: ωi→*ωm , de la misma manera para representar un proceso de una o más derivaciones es la siguiente: ωi→+ωm - Lenguaje generado por una gramática Conjunto de palabras definidas sobre el alfabeto Σ, que se obtienen desde el símbolo inicial en un proceso de derivación de cero o más pasos. S→ α1 → α2 → α3….→ x = x∈ Σ* L(G)={x | x∈ Σ* , S→*x} Lenguaje generado por la siguiente gramática: G = (Σ ={a, b}, N={S, A, B}, S, P) P: S → AB A → Aa |ε B →Bb |ε AaB S→AB B AbB A AaaB aB Aa AaBb Bb ε AabB bB AbBb Ab Aa ε …anbm | n≥1, m≥0 …. bm |m≥0 L(G)= {an bm | n≥0, m≥0} … anbm | n≥0, m≥1 ... an | n≥0 Forma sentencial – cualquier palabra obtenida de un proceso de derivación; en el anterior ejemplo cualquier palabra derivada desde S: S , AB, ε…..aaabb, son formas sentenciales Si la forma sentencial está compuesta únicamente por símbolos terminales de la gramática, se llama sentencia . En el ejemplo anterior son sentencias: ε, a , b, aa,…..aaabb A partir de la anterior definición, se puede decir que, el lenguaje generado por una gramática es el conjunto de sentencias que pueden obtenerse desde el símbolo inicial en un proceso de derivación. - Gramáticas equivalentes Una gramática genera un solo lenguaje, pero un lenguaje puede ser generados por muchas gramáticas. Dos o más gramáticas se dice que son equivalentes, si generan el mismo lenguaje. Ejemplo: G1 = (Σ ={a, b}, N={S, A, B}, S, P) P: S → AB A → Aa |ε B →Bb |ε 25 G2 = (Σ ={a, b}, N={S, A, B}, S, P) P: S→aA|bB |ε A → A a |ε | b B B → B b |ε Las gramáticas G1 y G2 son equivalentes. Ambas generan el mismo lenguaje: L={anbm | n,m≥0}={a0b0, a0b1, a1b0, a1b1,…. .} 2.7.- Clasificación de las gramáticas Noam Chomsky en 1959 clasificó las gramáticas en cuatro familias (Jerarquía de Chomsky) de gramáticas, que difieren, atendiendo a la forma que pueden tener sus reglas de producción. Sea una gramática G = (Σ, N, S, P) P: { α→β,..} α ∈ ( N | Σ)* N ( N | Σ)*, β ∈ ( N | Σ)* clasificaremos las gramáticas, en los siguientes cuatro tipos: - Gramáticas de Tipo 3 (Gramáticas regulares). Pueden ser a su vez, de dos tipos: Lineales por la derecha. Todas sus producciones son de la forma: A → aB A→a A →ε ¸ en donde A, B ∈ N , a ∈ Σ Lineales por la izquierda. Todas sus producciones son de la forma: A → Ba A→a A →ε ¸ en donde A, B∈ N , a ∈ Σ Los lenguajes generados por estas gramáticas se llaman lenguajes regulares o lenguajes de tipo 3 y, todos forman la clase de lenguajes L3. * Existen otras definiciones sobre las gramáticas de tipo 3, entre otras, como aquellas que permiten las producciones de la forma: A→aB, A→a y S→ ε (siempre que el lenguaje tenga la palabra vacía) En nuestro caso hemos cogido la más simple ya que permite una mayor flexibilidad en el tratamiento y transformación entre los mecanismos regulares. - Gramáticas de Tipo 2 (Gramáticas libres del contexto). Las producciones son de la forma: A →α donde A ∈ N , α ∈ ( N | Σ)* Los lenguajes generados por este tipo de gramáticas se llaman lenguajes libres del contexto o lenguajes de tipo 2 y, todos forman la clase de lenguajes L2. - Gramáticas de Tipo 1 (Gramáticas sensibles al contexto). Las producciones son de la forma: α →β en donde α ∈ ( N | Σ)* N ( N | Σ)*, β ∈ ( N | Σ)* |α|≤|β| Se permite además la producción S →ε ¸ siempre y cuando no aparezca el símbolo no terminal S en la parte derecha de ninguna regla de producción. El sentido de estas reglas de producción δAγ→δβγ es el de especificar que el símbolo A puede ser reemplazado por β, en una derivación directa sólo cuando A aparezca en el “contexto" de δAγ . 26 Las producciones de este tipo de gramáticas cumplen siempre que la parte izquierda tiene longitud menor o igual que la parte derecha, pero nunca mayor (excepto para S →ε ). Esto quiere decir que la gramática es no contráctil. Los lenguajes generados por las gramáticas de tipo 1 se llaman lenguajes sensibles al contexto y, forman el conjunto de lenguajes de tipo L1 - Gramáticas de Tipo 0 (Gramáticas con estructura de frase, no restringidas) Son las gramáticas más generales, que por ello también se llaman gramáticas sin restricciones. Esto quiere decir que las producciones pueden ser de cualquier tipo permitido, es decir, de la forma: α→β , α є ( N | Σ)*N ( N | Σ)* β є ( N | Σ)* Los lenguajes generados por estas gramáticas son los lenguajes con estructura de frase, que se agrupan en la clase de lenguajes de tipo de L0. Estos lenguajes también se conocen como lenguajes recursivamente enumerables. Relación de inclusión de las gramáticas: Tipo 3⊆ Tipo 2⊆ Tipo 1⊆ Tipo 0 Tipo 0 Tipo 1 Tipo 2 Tipo 3 2.8.- Determinación del tipo de una gramática Para determinar el tipo de una gramática G, basta por equiparar las producciones de dicha gramática a las producciones de las familias de los tipos de gramáticas, empezando por las producciones de tipo superior, de no ser así se baja de tipo hasta encontrar un tipo que coincida con la dada. Ejemplo, dada la siguiente gramática determinar el tipo de la misma. G = (Σ ={a,b}, N={S,A,B},S, P) P: S → AB A → Aa |ε B →Bb |ε La producción S → AB ∉ tipo 3 la gramática G no es de tipo 3 La producción S → AB ∉ tipo 3 pero si a las gramáticas de tipo 2, por lo que la gramática G es una gramática de tipo 2. 2.9.- Tipos de lenguajes formales De los tipos de familias de gramáticas creados por Noam Chomsky (Jerarquía de Chomsky) se obtiene una clasificación en los lenguajes que generan. Dado que un lenguaje, puede ser generado por varias gramáticas, gramáticas equivalentes que pueden ser de tipo diferente, existe un problema a la hora de relacionar lenguajes con gramáticas, que tipo de gramática asociar al lenguaje. Para solucionar la anterior indecisión, podemos aplicar el siguiente axioma a la hora de determinar el tipo del lenguaje generado por la gramática: un lenguaje es de tipo N (N=0,1,2,3), si existe una gramática con un tipo N que lo genera y no otra de un tipo N superior. Sea el lenguaje L(Σ)={anbm | n, m≥0}={a0b0, a0b1, a1b0, a1b1,….}, lenguaje que puede ser generado entre otras por las gramáticas G1 y G2 Ejemplo: G1 = (Σ ={a, b}, N={S, A, B}, S, P) P: S → AB , A → Aa |ε , B →Bb |ε G1 es una gramática de tipo 2 27 G2 = (Σ ={a, b}, N={S, A, B}, S, P) P: S → aA| bB |ε , A → Aa |ε |bB , B →Bb |ε G2 es una gramática de tipo 3 Para determinar el tipo al que pertenece el lenguaje, será el tipo de la gramática con el tipo superior que lo genere, para este caso el tipo del lenguaje L(Σ)={anbm | n, m≥0 } será de Tipo 3 (no existe una gramática de un tipo superior que lo genere). Sea el lenguaje L(Σ )={anbn | n≥0}={a0b0, a1b1, a2b2, a3b3,…. G = (Σ ={a,b}, N={S,A,B},S, P) P: S → aSb |ε El lenguaje anterior es de tipo 2, no existe una gramática de un tipo superior que lo genere. El conjunto de los lenguajes de tipo 3 (regulares) definidos sobre un alfabeto Σ, está incluido propiamente en el conjunto de los lenguajes tipo 2 ( libres de contexto) y, éste a su vez, está incluido propiamente en el conjunto de los lenguajes de tipo 1 (sensibles al contexto), que lógicamente está incluido en el conjunto de lenguajes tipo 0 (con estructura de frase). Relación de inclusión de los tipos de lenguajes: L3⊆ L2 ⊆ L1⊆ L0 L0 L1 L2 L3 Ejercicios de gramáticas 1- Obtener una gramática para cada uno de los siguientes lenguajes: a) ∑= {a, b, c}, L = {an bm cn | n, m >= 1} b) ∑= {a, b, c}, L = {an bm cn+2m | n >=0, m >= 1} c) ∑= {a, b, c}, L = {an bn+m cm | n >= 1, m >= 0} d) ∑= {a, b}, L = {an bm | n >= m >= 1} e) ∑= {a, b}, L = {an bm | n > m >= 0} f) ∑ = {a, b, c}, L = {am bn ck | m > n + k ; n, k >=0} g) ∑ = {a, b}, L = { anbm | m <= n <= 2m ; n, m >=0} h) ∑ = {a, b}, L = { anbm | n <>m ; n, m >0} 2- Construir una gramática que especifique el siguiente lenguaje: L = { w ∈ {0, 1, 2}* | w contiene exactamente dos o tres símbolos 0 en cualquier posición}. ¿Sería posible obtener una gramática regular o de tipo 3? 3- Para las gramáticas que se proponen a continuación, determinar el tipo de las mismas y el lenguaje generado. a) S → 0S | A A → 0B | 0 B → 1A b) S→ abA | bbB A → bC B→a C → cS | ε 28 4- Sea la siguiente gramática definida sobre el alfabeto ∑ = {a, +, = } S → BSa | aSa | a+a=aa Ba → aB B+ → +a Obtener el lenguaje generado por la misma 5 - Dado el alfabeto Σ= {a, +, = } y el siguiente lenguaje: L={an+am=an+m | n, m > 0}, construir una gramática de tipo 2 que genere dicho lenguaje. 6- Dado el alfabeto Σ = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + ,- ,*, / }, sea el lenguaje L formado por todas las expresiones aritméticas que se pueden formar con los símbolos de este alfabeto, donde 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 son operandos y , + ,- ,*, / son los operadores binarios de suma, resta, producto y división respectivamente. Por ejemplo, pertenecen a este lenguaje las palabras 1, 1+0 y 5+3*8/7, y no pertenecen: 11, +1 ó 1*/2. Determinar el tipo del lenguaje L, justificándolo mediante un mecanismo formal del tipo correspondiente. 7- Sea el lenguaje de sumas de números naturales sobre Σ = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + }. Ejemplos de palabras del lenguaje: 2+67+9870 ó 35 Ejemplos de palabras no válidas: 34+ ó 34++23 Representar mediante una gramática de tipo 2 los siguientes lenguajes: a) Sea L1 el lenguaje de sumas en el que los números naturales no pueden tener ceros a la izquierda. El número 0 se representará por un único cero, nunca por una secuencia de ceros. i. Ejemplo:10+0 ii. Ejemplo de palabra no válida: 03+12 b) Sea L2 el lenguaje de sumas en el que los números naturales pueden empezar por 0, aunque no sea el número 0, pero en cada suma se cumple siempre la condición de que el primer y el último número tienen siempre la misma longitud (al menos habrá dos números). i. Ejemplo: 10+1+01 ii. Ejemplos de palabras no válidas: 03+129 , 1 c) Sea L3 el lenguaje de sumas en el que los números naturales pueden empezar por 0, aunque no sea el número 0, pero en el que se cumple la condición de que al menos dos números tienen la misma longitud. i. Ejemplo: 0503+30+450+89+203+03 8 - Dado el alfabeto Σ={a,b,0,1}, sea el lenguaje L={w·(0|1)·v | v,w pertenecen a {a,b}+ y |w|=|v|}, en el que además se cumplen las siguientes restricciones: • Si el dígito central es 0, w deberá empezar por a • Pero si el dígito central es 1, entonces es v quien empezará por a. Ejemplos de palabras válidas: aba0bba, a0a, a0b, a1a, b1a, abb1abb Ejemplos de algunas palabras no válidas: b0a, ba0aa, a1b, ba1ba Obtener una gramática independiente del contexto que genere el lenguaje pedido, teniendo en cuenta las restricciones. 9- Con el alfabeto ∑={a,b,c} se define el lenguaje L = {w·cn | w pertenece a {a,b}+, n>0} en el que además se cumple que: • si w termina en a, entonces la longitud de w es mayor o igual que n • si w termina en b, entonces la longitud de w es menor o igual que n Obtener una gramática independiente del contexto que genere el lenguaje pedido, teniendo en cuenta las restricciones pedidas. 10 - Dado el alfabeto Σ= {a, b} sea el lenguaje: L = { vbbw | v, w ∈ {a,b}* y |v| >= |w| (la longitud de v es mayor o igual que la de w) } Obtener una gramática independiente del contexto que genere el lenguaje pedido, teniendo en cuenta las restricciones pedidas. 29