Sei sulla pagina 1di 187

Lenguajes de libre contexto

Gramáticas

Una gramática es otra forma de describir un lenguaje.

Ejemplo:
O  S P
S  A T
P  V
A  el
A  un
T  niño
T  perro
V  corre
V  camina
Gramáticas

Una derivación de "un niño corre":


O  S P
S  A T O  S P
P  V  A T P
A  el  A T V
A  un  un T V
T  niño  un T corre
T  perro  un niño corre
V  corre
V  camina
Gramáticas

Lenguaje descrito por esta


O  S P gramática:
S  A T
P  V L={"el niño corre", "el niño
A  el camina", "un niño corre", "un niño
A  un camina", "el perro corre, el perro
T  niño camina", "un perro corre", "un
T  perro perro camina"}
V  corre
V  camina
Gramáticas

T  niño

Variable o "no- Regla de Terminal


terminal" producción
Gramáticas

Otro ejemplo: Lenguaje:


S  aSb
L  {a b : n  0}
n n
S 
Algunas derivaciones:
S  aSb  ab
S  aSb  aaSbb  aabb
S  aSb  aaSbb  aaaSbbb
Este formalismo
 aaaaSbbbb  aaaabbbb permite describir
algunos lenguajes que
no son regulares.
Gramática: forma general

Una gramática es una tupla G=(V,T,S,P) donde

•T es un conjunto finito de símbolos terminales


(es el alfabeto en que estarán escritas las
palabras).

•V es un conjunto finito de variables (símbolos


que no aparecerán en la palabra final).

•S  V es la variable de inicio.

•P es un conjunto finito de reglas de producción


de la forma pq, donde p es de la forma
(V+T)*V(V+T)* y q es de la forma (V+T)*.
Derivaciones

•T={a,b}
S  aSb •V={S}
•S=S
S  •P={SaSb, S}
S  aSb  ab
Una derivación es la obtención de una palabra u a
partir de una palabra v, ambas pertenecientes a
(V+T)*, aplicando una regla de producción:

•Si la regla es pq, será aplicable sólo si p está


incluida en u, o sea, u=xpy.

•El resultado será v=xqy. Escribimos uv.


Derivaciones

Definimos * como la cerradura transitiva de .

Es decir, u * v ssi u1,u2,...,uk tales que


u  u1  u2  ...  uk  v

Además definimos que u * u para todo u.

Definimos el lenguaje descrito por la gramática


como el conjunto de todas las palabras de
terminales que pueden derivarse a partir de S:
L(G) = { wT*: S * w }

A veces cuando no haya confusión posible, anotaremos *


simplemente como 
Convención sobre notación

Notación: se suelen usar en este contexto

•Letras minúsculas del comienzo del alfabeto para


los terminales.

•Letras minúsculas del final del alfabeto para las


palabras (de terminales, o de terminales mezclados
con variables).

•Letras mayúsculas para las variables. Cuando por


algún motivo esto pueda inducir a confusión,
entonces se usan con corchetes: S. Casos típicos:
cuando es inevitable usar mayúsculas en los
terminales, o cuando una variable tiene un nombre
natural (ejemplo: predicado).
Gramáticas de libre contexto

El formalismo general de gramáticas es demasiado


poderoso :
puede describir lenguajes más complejos que
los que nos interesan (por ahora).

Estudiaremos en este capítulo las gramáticas de


libre contexto y sus lenguajes asociados (lenguajes
de libre contexto).

Nota: también lo traducen como “de contexto


libre”, “independientes del contexto”, etc, etc.
Gramáticas de libre contexto

Def.: una gramática se dice de libre contexto (GLC)


ssi en toda regla de producción pq, se tiene que
pV.

Es decir, las reglas son de la forma


Xw
con w(V+T)*.

¿Por qué interesan?


 Los lenguajes de programación, y (salvo algunas
construcciones particulares) los lenguajes
naturales, son de libre contexto!!
(Gramáticas regulares

Un caso aún más particular son las GLC en que se


pide que todas las reglas de producción sean de las
formas
SaT
ó Sa

Se les llama gramáticas lineales por la derecha.


Análogamente se definen las gramáticas lineales por
la izquierda, que tienen reglas de la forma STa, ó
bien Sa.

Una gramática es regular si es lineal por la derecha,


o es lineal por la izquierda.
Gramáticas regulares

L es un lenguaje regular ssi L=L(G) para una


gramática regular G.
S  aA
A
a
SB Nota: en la clase el
estado C estaba anónimo,
a y las producciones que
A  aC incluyen C estaban
resumidas en una sola,
C AaaB. Lo cambio aquí

S C  aB para respetar el formato


definido en la


a VF transparencia previa.
a B  bB
B
Ba
b Ir derivando la palabra corresponde
a ir cambiando de estado interno (la
variable), y escribiendo.
Gramáticas regulares)

Ojo: para que G sea regular debe ser lineal por la


derecha o por la izquierda, pero no puede mezclar
las dos formas. Si las mezcla, puede que lo que sale
ya no sea regular:

•SaT
•TSb
•S

genera {anbn, n0}, que es el clásico ejemplo de


lenguaje no regular.
GLC, ejemplos

Volvamos al caso general de GLC. Otro ejemplo:


S  Tb
T  aTb
T 
¿Lenguaje?  {anbn+1, n0}

Una notación conveniente: cuando el lado izquierdo


es el mismo, agrupamos varias reglas de producción
en una sola línea mediante “|”:
S  Tb
T  aTb | 
GLC, ejemplos

Más ejemplos:

S  SS | ( S ) | 
donde T={(,)}, V={S}. ¿Lenguaje?

S  (S)  ()
S  (S)  (SS)  ((S)S)  ((S)(S))  (()(S))  (()())

da el lenguaje de los paréntesis bien balanceados.


GLC, ejemplos

Más ejemplos:

S  aSa | bSb | 
donde T={a,b}, V={S}. ¿Lenguaje?

S  aSa  abSba  abbSbba  abbbba

 es el lenguaje de los palíndromes de largo par:


L={ w=u uR, u{a,b}*}

Ejercicio: modificar la gramática para que también


genere palíndromes de largo impar (p.ej., aba).
GLC, ejercicios

Ejercicios:

•¿Qué lenguajes describen las siguientes gramáticas?

G 1: G2:
S  XaaX S  XY
X  aX | bX |  X  aX | bX | a
Y  Ya | Yb | a

•Escriba una gramática que describa el lenguaje


de todas las expresiones regulares válidas
sobre el alfabeto {a,b}.
GLC "prototípicas"

Algunas formas típicas de LLC (lenguajes de libre


contexto):
•Recursivos
•Por partes
•Anidados

Algunos ejemplos para tener en cuenta (como


“principios de diseño”):

•Recursivo: {anbn, n0}


Lo generamos con algo de la forma SaSb
El de los palíndromes es análogo.
Idea: hay un "surtidor" al medio que emite letras de
manera simétrica.
GLC "prototípicas"

•Por partes: L={anbnambm, n0, m0}

Lo generamos con algo de la forma SXY

Luego a partir de X e Y generamos las dos partes


(aprovechando que no tienen relación).

En este caso L=L1L1, con L1={anbn, n0}, de modo que


X e Y pueden ser el mismo:

SXX
XaXb | 
GLC "prototípicas"

•Anidados: L={anbmambn, n0, m0}

Aquí las “partes independientes” son por un lado


anbn y por otro lado bmam, que está dentro de la
anterior.

Generamos primero lo exterior, luego lo interior.

En este caso particular, tanto lo interior como lo


exterior es del tipo recursivo.

SaSb | X
XbXa | 
GLC, un ejemplo más complejo

L={w{0,1}*: w tiene dos bloques de 0’s del mismo


tamaño}.

Permitidos: 01011, 001011001, 10010101001


No permitidos: 01001000, 01111

10010011010010110
inicio parte central final
00110100
A B C D

Cantidad de 0’s:
A: , ó termina en 1 •la misma a cada lado
C: , ó comienza con 1 •al menos uno
GLC, un ejemplo más complejo

 De modo que descomponemos por partes, y


luego aplicamos recursividad en B.

10010011010010110
inicio parte central final
A B C S → ABC
A →  | U1
00110100 U → 0U | 1U | 
C →  | 1U
D
B → 0B0 | 0D0
Cantidad de 0’s: D → 1U1 | 1
•la misma a cada lado
•al menos uno
Derivaciones y árboles

Cuando una derivación pasa por algún punto en que


hay más de una variable, significa que habrá varias
derivaciones equivalentes, según cuál sea el orden
en que aplicamos las producciones:

S  XY
X  aaX | 
Y  Yb | 
S XY aaXY aaXYb aaaaXYb aaaaXb aaaab

S XY aaXY aaaaXY aaaaY aaaaYb aaaab


Derivaciones y árboles

Dentro de esta variedad de


S  XY
derivaciones equivalentes,
distinguimos la derivación izquierda X  aaX | 
y la derivación derecha (anotadas
por L y R respectivamente).
Y  Yb | 

S XY aaXY aaaaXY aaaaY aaaaYb aaaab


es una derivación izquierda: en cada paso reemplazamos la
variable que está más a la izquierda.

Una derivación derecha sería:


S XY XYb Xb aaXb aaaaXb aaaab
Derivaciones y árboles

Árbol de análisis sintáctico (o “árbol de derivación”):


representa la derivación, sin importar el orden:
S
SL aaaab :

X Y S XY aaXY
aaaaXY aaaaY
aaaaYb aaaab
a a X Y b
SR aaaab :

a a X  S XY XYb
Xb aaXb
aaaaXb aaaab

Derivaciones y árboles
S
•En la raíz va S.
X Y
•En las hojas, terminales o .
a a X Y b •Las derivaciones extremas,
L y R, corresponden a
a a X  hacer recorridos del árbol
en pre-orden y post-orden,
 respectivamente.

•Nótese que en este caso hay más de una derivación, pero


el árbol es único (no hay otra forma de derivar aaaab).
•No siempre será el caso.
GLC: ambigüedad

E  E + E | (E) | V
E Vx|y|z E
E + E E + E
x+y+z
V E + E E + E V
x V V V V z
y z x y

E  E+E  V+E  x+E  E  E+E  E+E+E  V+E+E 


x+E+E  x+V+E  x+y+E  x+E+E  x+V+E  x+y+E  x+y+V
x+y+V  x+y+z  x+y+z

•Aquí hay dos árboles distintos, cada uno con una


derivación izquierda distinta.
GLC: ambigüedad

T o t a l = p r e c i o + i v a ; Analizador
léxico

Total = precio + iva ;

asignación

Parser
Total := Expresión

id + id

precio iva

•Los parseadores construyen árboles de análisis sintáctico.


•El árbol indica cómo se entiende el texto.
GLC: ambigüedad
La existencia de más de un árbol de derivación
puede ser entonces nefasta:

E  E + E | EE | (E) | V
E Vx|y|z E
E  E E + E
V E + E xy+z E  E V
x V V V V z
y z x y

“Primero y+z, “Primero xy,


luego x  eso” luego eso + z”
GLC: ambigüedad

Un caso clásico de ambigüedad en lenguajes de


programación: dos ifs, un else.

S La mayoría de los
lenguajes lo resuelven
if b then S else S
asignando el else al if
más cercano.
if b then S a

a
S

if b then S

if b then S else S

a a
GLC: ambigüedad

Decimos que una gramática G es ambigua, si existe


una palabra en L(G) que admite más de un árbol de
derivación.

Nota: puede haber más de una derivación sin que


indique ambigüedad, siempre y cuando sigan el
mismo árbol.

Otro motivo que hace nefasta la ambigüedad: para


el parser es más fácil encontrar un árbol de
derivación si la solución es única.
GLC: ambigüedad

•La buena noticia: a veces podemos cambiar la


gramática por otra equivalente (i.e., mismo
lenguaje) pero sin ambigüedad:

E  E + E | EE | (E) | V
Vx|y|z
•Genera lo mismo, pero
obliga al árbol a
ET|E+T reconocer la prioridad
TF|TF de la multiplicación.
F  (E) | V •Java o C++ aplican algo
Vx|y|z análogo para resolver
los else ambiguos.
GLC: ambigüedad

Las malas noticias:

•A veces un lenguaje es inherentemente


ambiguo: sólo existen gramáticas ambiguas que
lo describen.

•Si tenemos una gramática ambigua, no existe un


algoritmo general que nos diga acaso es
intrínsecamente ambigua.

•Y aún si no lo fuera, tampoco hay método


infalible para “desambiguarla”.
GLC: ambigüedad

Ejemplo de lenguaje inherentemente ambiguo (sin


demostrar):

L  {a nb nc m }  {a nb mc m }

S  S1 | S2 S1  S1c | A S 2  aS 2 | B
A  aAb |  B  bBc | 

•Una palabra de la forma anbncn tiene dos árboles distintos.


•Lo que no demostraremos es que para cualquier otra
gramática equivalente, pasa lo mismo.
Ver Hopcroft.
GLC: ambigüedad

Ergo:

Se puede tratar de evitar la ambigüedad.

A veces hay que convivir con ella.

Un lenguaje de programación debiera


diseñarse con una gramática que evite la
ambigüedad (tanto para evitar errores de
interpretación, como para facilitar el parseo).
GLC: simplificación

Dada una gramática G, digamos,

S → aSb | bSaSb | T
T→S|

si además nos dan un string w,

•¿Cómo sabemos acaso wL(G)?

•En caso de que esté, ¿cómo obtenemos un árbol


de derivación?

•Y en caso de obtenerlo, ¿es único?


GLC: simplificación

S → aSb | bSaSb | T w=aabbb


T→S| ¿está?

Por fuerza bruta: podemos intentar generar todas


las posibles derivaciones, buscando alguna que
genere w.

S  aSb  aaSbb 
 abSaSbb  ... Problema:
 aTb 
 bSaSb  baSbaSb  ¿Cuándo
...
parar?
 T  S  ...
  
GLC: simplificación

S → aSb | bSaSb | T w=aabbb


T→S| ¿está?

Una idea: parar cuando la palabra que tenemos


exceda la longitud de w.

Problema #1: si existen reglas de producción que


llevan a , entonces la longitud no necesariamente
va aumentando.
S  aSb  abSaSbb  abSabb  ababb

Para evitar esos "acortamientos", sería bueno que


no hubiera producciones nulas como esa.
GLC: simplificación

S → aSb | bSaSb | T w=aabbb


T→S| ¿está?

Una idea: parar cuando la palabra que tenemos


exceda la longitud de w.

Problema #2: podemos quedarnos pegados en loops,


si se dan casos en que, por ejemplo, S→T, T→S.

S  aSb  aTb  aSb  …

Así que sería bueno evitar situaciones de ese tipo


también.
Eliminación de producciones nulas

Decimos que una variable X es anulable si existe una


derivación
X * 

Para determinar las variables anulables, aplicamos un


algoritmo de marcado recursivo:

•Marcamos todas las variables X que tengan una


regla de producción X.

•Mientras exista una regla de producción de la


forma YX1...Xm donde Y no está marcado pero
todos los Xi lo están, marcar Y.
Eliminación de producciones nulas

Para eliminar las producciones nulas,

(1) Determinar todas las variables anulables,


X1,...,Xk

(2) Para cada producción de la forma YuXiv,


agregar una producción Yuv. Más en general: si
el lado derecho incluye más de una variable
anulable, considerar cada combinación de
anulación. Por ejemplo, si YuXabYv, y tanto X
como Y son anulables, agregamos
•YuabYv
•YuXabv
•Yuabv
Eliminación de producciones nulas

Para eliminar las producciones nulas,

(3) Si Xi es una producción, eliminarla.

(4) Si S es anulable, entonces agregar una


producción S (salvo que ya exista).

Esa última salvedad es importante:


•Si  no está en el lenguaje de la gramática,
entonces S no es anulable y no habrá producciones
nulas.
•Si  está en el lenguaje de la gramática, entonces
S es anulable y la única producción nula será S.
Eliminación de producciones nulas

Ejemplo:

S  a | Xb | aYa •Eliminamos X  
XY| •A partir de SXb se agrega
Yb|X Sb, pues X es anulable.
•A partir de SaYa se agrega
Anulables: X e Y. Saa, pues Y es anulable,

S  a | Xb | aYa | b | aa
XY
Yb|X
Eliminación de producciones nulas

Otro ejemplo:

Gramática inicial Gramática final

S  aMb S  aMb
Sustituimos
M  aMb M  S  ab
M  M  aMb
M  ab
Anulables: M
Eliminación de producciones unitarias

Una "producción unitaria" es de la forma XY;


decimos que existe una derivación unitaria entre dos
variables X e Y si se tiene X * Y.

Si no hay producciones nulas (y supondremos aquí que


ya las eliminamos), entonces X * Y sólo es posible
mediante una cadena de producciones unitarias.
Producción
S X -
S  aX | Yb S Y -
XS X S XS
Y  bY | b | X X Y -
Y X YX
Y S YXS
Eliminación de producciones unitarias

Para eliminar derivaciones unitarias:

•Para cada par de variables tales que X * Y,


introducimos nuevas producciones: para cada
producción no-unitaria de Y, Ys1, Ys2, ...,
agregamos Xs1, Xs2, ...

•Se hace simultaneamente para todos los pares X,Y


con derivación unitaria.

•Después se eliminan las producciones unitarias, y


todo lo redundante (que suele aparecer).
Eliminación de producciones unitarias

Ejemplo: S  aX | Yb
XS
Y  bY | b | X
•Como X * S, se agregan XaX , XYb
•Como Y * X... No se agrega nada.
•Como Y * S, se agregan YaX , YYb
•Finalmente, eliminamos las producciones unitarias
XS y YX.

La nueva gramática quedaría

S  aX | Yb
X  aX | Yb
Y  bY | b | aX | Yb
Eliminación de variables inútiles

Al ir transformando las gramáticas (manteniendo,


recordemos, el mismo lenguaje) pueden aparecer
variables o producciones inútiles. Dos formas típicas
son:

S  aSb SX
S  X  aX Y es inútil: no
SX X  hay forma de que
aparezca en una
X  aX Y  bX derivación!

X es inútil: nunca desaparece, así que no puede formar


parte de la derivación de una palabra del lenguaje.
Eliminación de variables inútiles

En general una variable será útil cuando exista una


palabra wL(G) en cuya derivación aparezca:

S    uXv    w
•Si esto nunca ocurre, es inútil.

•Una producción es útil sólo si todas sus variables son


útiles. De lo contrario, es inútil.

•Para determinar las variables inútiles: son aquellas


que no producen strings de terminales, o bien, que no
son accesibles desde S.
Eliminación de variables inútiles

Para encontrar las variables que producen strings de


terminales, definimos un conjunto U=T. Luego
iteramos:
•Si existe una variable XU, pero que tiene una
producción Xu, uU*, entonces agregamos X a U.
•Si no existe ninguna variable así, salimos de la
iteración.

S  aS | A | C U={a,b}
Aa U={a,b,A}
U={a,b,A,S}
B  aa
U={a,b,A,S,B}
C  aCb
Eliminación de variables inútiles

Las variables que hayan quedado fuera de U son las


que no producen strings de terminales, ergo, son
inútiles. Las eliminamos, así como todas las
producciones en que aparezcan.

S  aS | A | C U={a,b} S  aS | A
Aa U={a,b,A}
U={a,b,A,S}
Aa
B  aa
U={a,b,A,S,B} B  aa
C  aCb
Eliminación de variables inútiles

A continuación eliminamos las variables que no se


alcancen desde S. Eso es aplicar recorrido de grafos,
de EDA:

S  aS | A
Generamos
Aa grafo de
dependencia S A B
B  aa

B no es alcanzable
 Es inútil. S  aS | A
Aa
Eliminación de variables inútiles

Simplificación de gramáticas:

(1) Eliminar producciones nulas

(2) Eliminar producciones unitarias

(3) Eliminar variables inútiles

• (1) y (2) son relevantes para que las cosas que


siguen a continuación funcionen.

• (3) es útil para no acarrear lastre, que suele


aparecer como subproducto de (1) y (2).
Aumento de longitud en la derivación

Sea G una gramática sin producciones nulas ni


unitarias.

Entonces, en una derivación, cada paso aumenta la


longitud de la palabra, o bien lo mantiene constante
pero a costa de reducir la # de variables.
¿Motivo? Cada paso de la derivación reemplaza una variable
X por algo, pero ese "algo" no es  ni tampoco es una
variable "desnuda". Alternativas:
•Reemplazar por una expresión con más de un símbolo:
la expresión se alarga.
•Reemplazar por un símbolo terminal: queda con la
misma longitud, pero con una variable menos.
Aumento de longitud en la derivación

En ese caso sí podemos aplicar la fuerza bruta para


evaluar acaso un string w pertenece a una gramática G:

•Probamos todas las derivaciones de largo a lo más


|w| (es decir, todas las combinaciones de a lo más
|w| reglas de producción).

•Esa es ahora una cantidad finita, así que podemos


probarlas en tiempo finito. En el peor de los casos,
probamos |P|+|P|2+...+|P||w| combinaciones.

•wL(G) ssi alguna de esas derivaciones la genera.


La buena y mala noticia

Es buena noticia: nos da un algoritmo para saber si una


palabra está en el lenguaje.

La mala noticia: es pésimo.

La cantidad de casos crece exponencialmente en


la medida que crece |w|.
Para un código Java de 200 líneas, el tiempo de
ejecución sería astronómico ( 10200).

Existe un algoritmo más eficiente, pero requiere


transformar la gramática un poco más:
Forma Normal de Chomsky (FNC)

Una gramática está en la forma normal de Chomsky


(FNC) si cada producción (a excepción de S, si que
existe) es de una de las dos siguientes formas:

XYZ o bien Xa

donde, como de costumbre, X,Y,ZV, aT.

S  AS S  AS
Está en FNC
S a S  AAS
A  SA A  SA
No está en FNC
Ab A  aa
Forma Normal de Chomsky (FNC)

Teorema: Para toda gramática de libre contexto G,


existe una gramática de libre contexto G' en forma
normal de Chomsky que es equivalente a G (es decir,
L(G)=L(G')).

Para demostrarlo, tenemos que ver que podemos


transformar cualquier GLC hasta que quede en FNC.

1. Eliminamos las producciones nulas, unitarias, e


inútiles.
2. Eliminamos los lados derechos "mixtos":
Forma Normal de Chomsky (FNC)

Para eliminar los lados derechos mixtos, creamos una


nueva variable Ta por cada terminal a. Reemplazamos
cada a por el Ta respectivo, y agregamos un
producción Taa.
S  ABTa
S  ABa
A  TaTaTb
A  aab
B  ATc
B  Ac
Ta  a
Ojo: si alguna producción ya era de la
forma Xa, la dejamos así (no la
Tb  b
cambiamos a XTa). Así evitamos Tc  c
introducir producciones unitarias.
Forma Normal de Chomsky (FNC)

3. Reemplazamos toda producción de la forma


AC1C2...Cn por una cadena de producciones
AC1V1, V1C2V2, ... , Vn-2Cn-1 Cn
donde los V1,...,Vn-2 son nuevas variables, intermedias.
S  AV1
S  ABTa V1  BTa
A  TaTaTb A  TaV2
V2  TaTb
B  ATc
B  ATc
Ta  a
Ta  a
Tb  b Tb  b
Tc  c Tc  c
Forma Normal de Chomsky (FNC)

Y listo.
S  AV1
•Llevar GLC a la forma normal de V1  BTa
Chomsky es relativamente fácil. A  TaV2
V2  TaTb
•Tener la GLC en FNC sirve para varias
cosas, prácticas y teóricas. B  ATc
Ta  a
•La más importante: para parsear en Tb  b
tiempo polinomial en |w|.
Tc  c
CYK

Algoritmo CYK (Cocke-Younger-Kasami):

Input: una GLC G en FNC, y una palabra w.

Output: acaso wL(G)

[y fácilmente se puede pedir que además dé un árbol


de derivación, en caso de respuesta positiva]

Idea: determino las variables que producen todas las


subpalabras de w de largo 1. Luego las que producen
todas las subpalabras de w de largo 2. Etc...
CYK

•Para una palabra de largo k dada (digamos, u),


consideramos las posibles formas de descomponerla en 2
palabras más cortas.

•Si para una descomposición u=v1v2 se tiene que X * v1,


Y * v2 (esa información ya está en la tabla), y existe
ZXY (eso lo miro en la gramática), entonces Z*u.

Al terminar: wL(G) ssi S es una de las variables que


producen w.
CYK
Ejemplo,
con gramática: Subpalabras de largo k,
S  AB partiendo de posición j

A  BB k\j 1 2 3 4 5
Aa 1 a a b b b

B  AB 2 aa ab bb bb
Bb
3 aab abb bbb

y palabra 4 aabb abbb

aabbb
5 aabbb
S  AB
CYK
A  BB
Aa
a a b b b
B  AB
A A B B B
Bb
aa ab bb bb

aab abb bbb


Variables que
generan las
subpalabras de aabb abbb
largo 1

aabbb
S  AB
CYK
A  BB
Aa
a a b b b
B  AB
A A B B B
Bb
aa ab bb bb
S,B A A
aab abb bbb
Variables que
generan las
subpalabras de aabb abbb
largo 2

aabbb
S  AB
CYK
A  BB
a a b b b
Aa
A A B B B
B  AB
Bb aa ab bb bb
S,B A A
aab abb bbb
Variables que S,B A S,B
generan las
subpalabras aabb abbb
de largo 5 (o A S,B
sea, w)
aabbb
S,B
CYK

•Es fácil modificar CYK para que me entregue un árbol


de derivación: cada vez que ponemos una variable en la
tabla, debemos recordar por qué la pusimos.

•Luego con esa información recuperamos el árbol.

•CYK llena una tabla de tamaño  |w|2.

•Para cada item de la tabla hay que hacer algo de


trabajo. Se puede demostrar que el tiempo de ejecución
es  |w|3. Mucho mejor que |P||w|.
PDA (Autómatas de pila)

•Antes teníamos la correspondencia lenguajes regulares


 autómatas finitos.

•También para los LLC existe, no sólo un tipo de


gramática, sino también un tipo de máquina que los
reconoce.

•Como los lenguajes regulares son un subconjunto propio


de los LLC, el nuevo tipo de máquina es una
generalización del que teníamos antes.
PDA (Autómatas de pila)

Input

Estados Pila

PDA

Al AF le agregamos una pila (stack). Memoria


potencialmente infinita, pero de acceso restringido.
PDA (Autómatas de pila)

•El resultado es un autómata de pila, autómata apilador,


o en inglés pushdown automaton (PDA).

•A medida que el PDA lee su input, puede sacar o


guardar símbolos en la pila.

•Sus cambios de estados, y lo que haga con la pila,


dependerán de lo que va leyendo en el input y de lo que
saque de la pila.

PDA
PDA (Autómatas de pila)

No determinismo:
•Los PDA que veremos, salvo que se indique lo
contrario, son no deterministas : hay transiciones ,
puede haber más de una transición para una misma
situación, etc...

Lenguaje reconocido:
•El lenguaje reconocido por el PDA será el conjunto
de palabras para las cuales existe una secuencia de
transiciones que concluye con la palabra leída
completa, y en estado de aceptación.
•Es decir, igual que en AFND+.
PDA (Autómatas de pila)

Pila •La pila tiene su propio alfabeto,  (que puede


coincidir en parte con el del input).

•Existe un símbolo especial $   que señala "el


fondo" de la pila (algunos libros usan z0, u otro).
$ Al comienzo es lo único que la pila contiene.

saco de guardo en
leo del
la pila la pila
•En el grafo de input
transiciones se anota
lo que se saca y
guarda en la pila. q1 a, b  c q2
a, b  c
q1 q2

input
 a   a 

pila
b tope c
h Reemplaza h
e e
$ $
a,   c
q1 q2

input
 a   a 

pila c
b tope b
h Guarda h
e ("push")
e
$ $
a,b  
q1 q2

input
 a   a 

pila
b tope
h Saca h
e ("pop")
e
$ $
q1 a,   q2

input
 a   a 

pila
b tope b
h Sin cambio h
e e
$ $
PDA (Autómatas de pila)

q2
a, b  c

q1 q1  ,b  c q2

a, b  c q3
No determinismo Transición 
PDA (Autómatas de pila)

Formalmente, un PDA es una tupla

M = ( Q,  , q0, F )

•Q es un conjunto finito de estados


• es el alfabeto de entrada
• es el alfabeto de la pila
•q0 es el estado inicial
•FQ son los estados de aceptación
• es la función de transición,

: Q({})({})  2Q({})
PDA, ejemplo

Recordatorio:  también se escribe a


Ejemplo:
veces como “”, o también como “”.

En los monos que siguen, está como 


porque eran muchos para cambiarlos!

a,   a b, a  

q0 ,    q b, a   q  , $  $ q
1 2 3
PDA, ejemplo
Input
a a a b b b
$
Pila

estado a,   a b, a  
actual

q0  ,    q b, a   q  , $  $ q
1 2 3
PDA, ejemplo
Input
a a a b b b
$
Pila

a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA, ejemplo
Input
a a a b b b a
$
Pila

a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA, ejemplo
Input a
a a a b b b a
$
Pila

a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA, ejemplo a
Input
a
a a a b b b a
$
Pila

a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA, ejemplo a
Input
a
a a a b b b a
$
Pila

a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA, ejemplo
Input a
a a a b b b a
$
Pila

a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA, ejemplo
Input
a a a b b b a
$
Pila

a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA, ejemplo
Input
a a a b b b
$
Pila

a,   a b, a  
acepta
q0  ,    q1 b, a   q2  , $  $ q3
PDA, ejemplo

En general, el lenguaje que este PDA acepta es

L  {a b : n  0}
n n

a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA, otro ejemplo

Otro ejemplo, ahora con lenguaje:

L( M )  {ww } R

a,   a a, a  
b,   b b, b  

q0 ,    q1 , $  $ q2
PDA, otro ejemplo
Input
a b b a
$
Pila
a,   a a, a  
b,   b b, b  

q0 ,    q1 , $  $ q2
PDA, otro ejemplo
Input
a b b a a
$
Pila
a,   a a, a  
b,   b b, b  

q0 ,    q1 , $  $ q2
PDA, otro ejemplo
Input
b
a b b a a
$
Pila
a,   a a, a  
b,   b b, b  

q0 ,    q1 , $  $ q2
PDA, otro ejemplo
Input
b
a b b a a
$
Pila
a,   a a, a  
b,   b b, b  

q0 ,    q1 , $  $ q2
PDA, otro ejemplo
Input
b
a b b a a
$
Pila
a,   a a, a  
b,   b b, b  

q0 ,    q1 , $  $ q2
PDA, otro ejemplo
Input
a b b a a
$
Pila
a,   a a, a  
b,   b b, b  

q0 ,    , $  $ q2
q1
PDA, otro ejemplo
Input
a b b a
$
Pila
a,   a a, a  
b,   b b, b  
acepta
q0 ,    q1 , $  $ q2
Un ejemplo más:
Vimos que la gramática de abajo
genera el lenguaje L={w{0,1}*: w tiene D
2 bloques de 0’s del mismo tamaño}
[transparencia 23]

10010011010010110
Ejemplos de strings en L: 01011, 001011001,
A B C
10010101001
Ejemplos de strings fuera de L: 01001000, A, parte inicial: ,
01111 o termina en 1
S → ABC
B, parte central: zona
A →  | U1 D, rodeada por
U → 0U | 1U |  bloques de 1 o más 0’s.
C →  | 1U C, parte final: ,
o comienza con 1
B → 0B0 | 0D0
D → 1U1 | 1 D, centro del centro:
parte y termina con 1.
Un ejemplo más:
He aquí un PDA que reconoce ese
mismo lenguaje:
B
A 0, / 
1, /  0,  / 0

, /  1, /  0,  / 0 D
q0 q1 q2 q3
1, / 
, /  0, / 
0, /  1, /  q4
1, / 
1, / 
1, $ / $ 1, / 
q6 q5

C 0, 0 / 
q7 , $ / $
Guardando strings
String guardado
Permitiremos ahora
guardar strings en la
pila: q1 a, b  w q2

a,b  cda
q1 q2
c string
pila d guardado
b tope Guarda a
h “cda” h
e e
$ $
Guardando strings

El poder de cómputo no cambia, pues

a,b  cda
q1 q2

es equivalente a tener

a,b  a q aux  ,  d aux  ,  c


q1 1 q 2 q2
Y otro ejemplo...

L( M )  {w : na  nb }

a, $  0$ b, $  1$
a, 0  00 b, 1 11
a, 1   b, 0  

q1
, $  $ q2
Input
a b b b a a

$
a, $  0$ b, $  1$
Pila
a, 0  00 b, 1 11
a, 1   b, 0  

q1
, $  $ q2
Input
a b b b a a
0
$
a, $  0$ b, $  1$
Pila
a, 0  00 b, 1 11
a, 1   b, 0  

q1
, $  $ q2
Input
a b b b a a
0
$
a, $  0$ b, $  1$
Pila
a, 0  00 b, 1 11
a, 1   b, 0  

q1
, $  $ q2
Input
a b b b a a
1
$
a, $  0$ b, $  1$
Pila
a, 0  00 b, 1 11
a, 1   b, 0  

q1
, $  $ q2
Input
a b b b a a 1
1
$
a, $  0$ b, $  1$
Pila
a, 0  00 b, 1 11
a, 1   b, 0  

q1
, $  $ q2
Input
a b b b a a 1
1
$
a, $  0$ b, $  1$
Pila
a, 0  00 b, 1 11
a, 1   b, 0  

q1
, $  $ q2
Input
a b b b a a
1
$
a, $  0$ b, $  1$
Pila
a, 0  00 b, 1 11
a, 1   b, 0  

q1
, $  $ q2
Input
a b b b a a

$
a, $  0$ b, $  1$
Pila
a, 0  00 b, 1 11
a, 1   b, 0  
acepta
q1
, $  $ q2
•Idea: este PDA guarda en la pila sólo lo que está en
exceso, respecto a na=nb.

•Se usó esta vez 0 y 1 en la pila, para representar a y b


respectivamente. Esto es frecuente: distinguir los
alfabetos ayuda a no confundirse.

a, $  0$ b, $  1$
a, 0  00 b, 1 11
a, 1   b, 0  

q1
, $  $ q2
PDA: descripción instantánea

Tiempo atrás definimos la configuración o descripción


instantánea de un AF (la “foto” del AF en un momento
dado) como (q,w), donde q era el estado actual y w
era la parte del input que faltaba por leer.

Es similar para PDA, pero ahora la “foto” debe incluir


el contenido de la pila.

(q,w,v)

q  Q, el
v  , el contenido
estado actual
w  , lo que falta actual del stack
por leer en el input
PDA: descripción instantánea

Descripción
instantánea
(q1, bbb, aaa$)
a
Instante 4: Input a
a a a b b b a
$
Pila
a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA: descripción instantánea

Descripción
instantánea
(q2 , bb, aa$)
a
Instante 5: Input a
a a a b b b a
$
Pila
a,   a b, a  

q0  ,    q1 b, a   q2  , $  $ q3
PDA: descripción instantánea

Escribimos (q1,bbb,aaa$)(q2,bb,aa$)
y el cómputo completo será
(q0,aaabbb,$)
 (q1,aaabbb,$)
 (q1,aabbb,a$)
 (q1,abbb,aa$)
 (q1,bbb,aaa$) •Abreviando podemos decir que
 (q2,bb,aa$) (q0,aaabbb,$)  (q3,,$)
 (q2,b,a$)
 (q2,,$) •A veces se pone * para
 (q3,,$) especificar que se refiere a
más de un paso.
Lenguaje por estado de aceptación

Podemos entonces definir formalmente el lenguaje


aceptado por un PDA M=(Q,  , q0, F) como

L(M) = { w*: qfF,v*, (q0,w,$)(qf,,v) }

Recuérdese que los PDA son no-deterministas, así que


para que wL(M) basta que exista alguna cadena de
cómputos que sirva.
Lenguaje por pila vacía

Otra forma de asociar un lenguaje a un PDA es


definir

N(M) = { w*: qQ, (q0,w,$)(q,,$) }

Es decir: el “estado de aceptación” es cualquier


estado, pero con la pila vacía, y la palabra de entrada
completamente leída.

Se demuestra (no es difícil) que las nociones son


equivalentes: si L=L(M1) para un PDA M1, existe un
PDA M2 con L=N(M2), y viceversa:
Lenguaje por pila vacía

Si tenemos un PDA que reconoce por estado de


aceptación, y queremos uno que reconozca por pila
vacía, hacemos que desde los estados de aceptación
pase (con ) a un estado de "limpieza" de la pila.

$ $

Ese z0 es un "falso piso", de


modo que la pila sólo estará
de verdad vacía al llegar a p.
Lenguaje por pila vacía

Al revés, si tenemos uno que reconoce por pila vacía, y


queremos uno que reconozca por estado de
aceptación: desde todos los estados ponemos una
transición  y se vaya a un estado de aceptación.
, $/

, $/
, $/Z0$

, $/

, $/
PDA y GLC

Teorema: L es reconocido por algún PDA ssi L es un


lenguaje de libre contexto.

Demostración, sólo la idea:

•Hacia un lado es fácil: veamos que si G es una


gramática de libre contexto, entonces existe un
PDA M con L(M)=L(G).

•Primero, recordemos como es una derivación


izquierda:
GLC  PDA
G:
S  aSa | YZ Consideremos una derivación izquierda
Y  bY |  de aabbcbaa con la gramática G.
Z  cZ | Y

S S Nótese que:
aSa a S a
•Lo que va
aaSaa aa S aa
quedando a la
aaYZaa aa Y Zaa
izquierda se
aabYZaa aab Y Zaa
“congela”, y no se
aabbYZaa aabb Y Zaa
vuelve a mirar.
aabbZaa aabb Z aa
aabbcZaa aabbc Z aa •Lo que está a la
aabbcYaa aabbc Y aa derecha es una
aabbcbYaa aabbcb Y aa pila!
aabbcbaa aabbcb aa
GLC  PDA

Sea G=(V,T,S,P) una gramática de libre contexto.


Construimos el PDA M=(Q,  , q0, F) con
•Q={q0, q1, q2}
•F={q2}
•=T
•=VT{$}
• se construye como sigue:

Para cada
Para cada
producción , p  q a, a   aT
pq en P

q0 ,   S q1  ,$  $
q2
GLC  PDA

Idea:
•Lo que está en la pila es lo que falta “procesar”.
•Si vemos ahí un terminal, significa que lo generó
nuestro último reemplazo. Por lo tanto, tiene que
tenerlo el input.
•Si es una variable, la tenemos que reemplazar por
alguna de sus producciones.

, p  q a, a  

q0 ,   S q1  ,$  $
q2
GLC  PDA
Ejemplo: derivación izquierda (q0,abcba,$) 
S (q1,abcba,S$) 
(a la idem), y cómputo en el
aSa (q1,abcba,aSa$) 
PDA (a la derecha).
aYZa (q1,bcba,Sa$) 
Gramática: (q1,bcba,YZa$) 
abYZa (q1,bcba,bYZa$) 
abZa S  aSa | YZ (q1,cba,YZa$) 
abcZa Y  bY |  (q1,cba,Za$) 
abcYa Z  cZ | Y (q1,cba,cZa$) 
abcbYa (q1,ba,Za$) 
, Y   (q1,ba,Ya$) 
abcba (q1,ba,bYa$) 
 , S  aSa  , Z  cZ
a, a   (q1,a,Ya$) 
 , S  YZ  , Z  Y b, b   (q1,a,a$) 
 , Y  bY c, c   (q1,,$) 
(q2,,$)

q0 ,   S q1  ,$  $
q2
PDA  GLC

En dirección contraria es más complicado.

No lo veremos en detalle.


Cada libro da una versión un poco distinta.
Todas son variaciones en torno a lo que sigue;
para dudas, ver el Hopcroft en inglés (v. 2).

Consideremos un PDA M=(Q,  , q0, F) en la forma


en que los definimos al comienzo (escribiendo
símbolos de a uno en la pila, no strings).
PDA  GLC

Las transiciones entonces son de la forma

p a, X  Y q
donde la a, la X y/o la Y pueden eventualmente ser .

•Usaremos aquí mayúsculas para los elementos de ,


para distinguirlos de los terminales en la gramática.

•Supondremos que el PDA acepta por pila vacía.

•Queremos una gramática que genere el mismo


lenguaje.
PDA  GLC

p a, X  Y q
•En la gramática que se define, los terminales serán 
(el alfabeto de entrada del PDA).

•El símbolo de inicio, S, será un símbolo especial.

•El resto de las variables corresponderán a los


elementos de Q({})Q; los escribiremos de la
forma [pXq].

 V = {S}  Q({})Q
PDA  GLC

p a, X  Y q
Las reglas de producción se definen con la intención
de que [pXq] produzca exactamente el conjunto de
palabras que, leídas en el PDA, pueden llevarnos desde
el estado p hasta el estado q, sacando una X de la pila
(y sin meternos más abajo):

[pXq] * w  (p, w, X) * (q, , )


PDA  GLC

p a, X  Y q
[pXq] * w  (p, w, X) * (q, , )
gramática pda

•Primero se define S[q0Z0p] para todo pQ.

•Ese Z0 es el "falso piso": al sacarlo, se vacía la pila, y


podemos aceptar (si el input se acabó).

•Si los [q0Z0p] cumplen lo de arriba, estamos bien.


PDA  GLC

p a, X  Y q r
A continuación, para cada transición como la de
arriba, se agregan las reglas de producción

[pXr]  a [qYr] para todo rQ

Idea: una forma de ir de p a r sacando una X de la


pila, es leer una a del input, y luego ir de q a r de
alguna forma que saque Y de la pila.

Nota: Si a=, tendremos [pXr]  a [qYr].


PDA  GLC

Para hacer la demostración, se demuestra

[pXq] * w  (p, w, X) * (q, , )

Cada implicación se demuestra por inducción; ver


Hopcroft en caso de incredulidad.

Esta construcción no se usa mucho; la otra tampoco.

Sirven básicamente para convencerse de la


equivalencia PDAGLC.
PDA y determinismo

Un DPDA es un deterministic pushdown automaton: lo


mismo que un PDA, pero sin ambigüedad en ningún
paso.

No lo demostraremos formalmente, pero se puede


demostrar lo siguiente: si anotamos por LM la clase de
lenguajes reconocidos por cada tipo de autómata,
tenemos que

LAFND+ = LAFD  LDPDA  LPDA

y las inclusiones son propias. Así que acá el no-


determinismo sí cambia los lenguajes.
PDA y determinismo

Un ejemplo de lenguaje que está en LDPDA pero no está


en LAFD es

{anbn:n0}

a,   a b, a  

q0 a,   a q1 b, a   q2  , $  $ q3

La primera b "me avisa" que tengo que cambiar de


estado. Gracias a eso es determinista.
PDA y determinismo

Un ejemplo de lenguaje que está en LPDA pero no está


en LDPDA es

{w=u uR : u{a,b}* }

Idea:

•Para reconocer 0n10n necesito que el segundo grupo


de 0's "mate" al primero en el stack

•...pero entonces ya no podría reconocer 0n10n0n10n,


que también está en L.
Lema de bombeo para LLC

Ahora veamos ejemplos de lenguajes que no están en


LPDA; es decir, que no son de libre contexto. Para eso,
la herramienta de demostración será el:

Lema de bombeo para LLC: sea L un lenguaje de libre


contexto. Entonces existe un nN tal que si wL,
|w|>N, entonces w puede descomponerse como
w = uvxyz
de modo tal que
•|vxy|  n, |vy| > 0
•uvkxykz k0
Lema de bombeo para LLC

w = uvxyz  |vxy|  n, |vy| > 0, uvkxykz k0

Idea:
•En la derivación, alguna variable R debe aparecer en la
producción a partir de si misma.
•Eso permite bombear: en lugar de producir Rx la
segunda vez que aparece, volvemos a hacer Rvxy, y así
tantas veces como queramos.
Lema de bombeo para LLC

B B

S b S b

A B A B

a B b b
a B b b
S b
b
A B

a B b b
Lema de bombeo para LLC

La idea, más en detalle:

•Si L es LLC, existe una gramática G=(V,T,S,P) en


forma normal de Chomsky que lo genera.

•Si el árbol de derivación de una palabra w es de


altura h, entonces |w|2h (pues en el caso extremo,
voy reemplazando cada símbolo por 2).

•Sea n=2|V|+2. Si |w|n, entonces su árbol de


derivación es de altura al menos |V|+2.

•Consideremos un camino de longitud máxima desde


S hasta una hoja (terminal).
Lema de bombeo para LLC

•Tiene que haber al menos alguna variable repetida a


lo largo de ese camino.

•Escogemos R como la última repetición


que vemos, al ir de arriba hacia abajo.

•uvkxykz k0 sale de lo anterior, y del mono.

•|vy| > 0, pues de lo contrario habría producciones


unitarias (y no hay, pues estamos en FNC).

•|vxy|  n, pues de lo contrario R no era la última


repetición (se aplicaría a |vxy| el argumento inicial).
"QED"
Lema de bombeo para LLC, ejemplo

Ejemplo: L={anbncn: n0} no es de libre contexto.

•Supongamos que sí lo es, y sea m la constante de


bombeo.

•Tomemos w=ambmcm.

•w tendría una descomposición w = uvxyz con


|vxy|  m, |vy| > 0, uvkxykz k0.

•Nótese que wL, |w|a=|w|b=|w|c.


Lema de bombeo para LLC, ejemplo

Ejemplo: L={anbncn: n0} no es de libre contexto.

•Para que uv2xy2z esté en L, vy debe cumplir


también |vy|a=|vy|b=|vy|c

•Pero como |vxy|  m, vy no puede incluir letras


de los tres tipos!

 ¡Contradicción!

Ejercicio: Usar bombeo para demostrar que el


lenguaje {w=u u : u{a,b}* } no es de libre contexto.
Lema de bombeo para LLC

Warning:

•Las demostraciones por bombeo en LLC suelen ser


bastante más complicadas que para lenguajes
regulares (el ejemplo aquí fue excepcionalmente
corto).

•Nótese que no sabemos nada sobre el u en w=uvxyz.


Por lo tanto, a diferencia del bombeo en lenguajes
regulares, no podemos asegurar que lo que
bombeamos esté al comienzo.

•Eso obliga, en general, a considerar hartos casos


(de posibles ubicaciones de vxy dentro de w).
Propiedades de clausura

Sean L1 y L2 dos lenguajes de libre contexto, descritos


por las gramáticas G1=(V1,T1,S1,P1) y G2=(V2,T2,S2,P2).

Entonces:

•L1  L2 es de libre contexto

En efecto, lo genera la gramática G=(V,T,S,P) con

•V = V1  V2  {S}
•T = T1  T2
•S una variable nueva
•P = P1  P2  {S S1|S2}
Propiedades de clausura

De manera similar,

•L1L2 es de libre contexto

Tomamos G=(V,T,S,P) con

•V = V1  V2  {S}
•T = T1  T2
•S una variable nueva
•P = P1  P2  {S S1S2}
Propiedades de clausura

También se tiene que:

•L1* es de libre contexto

Tomamos G=(V,T,S,P) con

•V = V1  {S}
•T = T1
•S una variable nueva
•P = P1  {S S S1 | }
Propiedades de clausura

Por otro lado,

•L1  L2 no necesariamente es de libre contexto

Contraejemplo:

L1={anbncm, n0, m0} y L2={anbmcm, n0, m0}


son de libre contexto [ejercicio!], pero

L1  L2 = {anbncn: n0}

no lo es.
Propiedades de clausura

También se tiene que

•L1C no necesariamente es de libre contexto.

Demostración:

•Los LLC son cerrados bajo la unión.


•Si además fueran cerrados para el complemento,
entonces serían cerrados para la intersección (por
leyes de Morgan!).
•Y acabamos de ver que no lo son.
Propiedades de clausura

Lo que sí se cumple es que para L3 regular,


 L1  L3 es de libre contexto

Idea de la demostración:
•Consideramos M=(Q,  , q0, F) un PDA que
reconoce L1, y M'=(Q', ', q'0, F') un AFD que
reconoce L3.
•Construimos un PDA con estados QQ', que
simulará simultáneamente M y M' sobre un mismo
input.
•Aceptamos la palabra si al final tanto M como M'
aceptan.
Propiedades de clausura

Lo que sí se cumple es que para L3 regular,


L1  L3 es de libre contexto

Aplicaciones de eso:

1) Probemos que L={anbn: n100} es de libre contexto.

Notamos que L = L1  L3, donde


• L1 = {anbn}
• L3 = {a,b}* \ {a100b100}
Sabemos que L1 es un LLC; por otro lado, L3 es
regular.  L es regular.
Propiedades de clausura

Lo que sí se cumple es que para L3 regular,


L1  L3 es de libre contexto

Aplicaciones de eso:

2) Probemos que
L={w{a,b,c}*: |w|a=|w|b=|w|c}
no es de libre contexto.

Por contradicción: si lo fuera, al intersectarlo con


a*b*c* (que es regular!) obtendríamos un LLC.

Pero L  a*b*c* = {anbncn}, que no es LLC.


Problemas de decisión asociados a LLC

Problema de membresía:

Dada una gramática de libre contexto G y una palabra


w, ¿wL(G)?

Respuesta:

•Algoritmo de parseo exhaustivo (requiere haber


eliminado producciones nulas y unitarias).

•Algoritmo CYK (requiere FNC).


Problemas de decisión asociados a LLC

Problema de vacuidad:

Dada una gramática de libre contexto G, ¿L(G)=?

Respuesta:

•Aplicar la eliminación de variables inútiles.

•Ver acaso S es una variable inútil.


Problemas de decisión asociados a LLC

Problema de finitud:

Dada una gramática de libre contexto G, ¿es L(G)


infinito?

Respuesta:

•Eliminar variables inútiles, producciones nulas,


producciones unitarias.
•Hacer un grafo de dependencia entre las variables.
•L(G) es infinito ssi existe algún ciclo en ese grafo.
Problemas de decisión asociados a LLC

Problema de finitud:

S  AB
A C
A  aCb | a
B  bB | bb S
C  cBS B

S  AB  aCbB  acBSbB  acbbSbbb


S * acbbSbbb * (acbb) 2 S (bbb) 2 * (acbb)i S (bbb)i
Problemas de decisión asociados a LLC

Problema de igualdad:

Dadas dos gramáticas de libre contexto G1 y G2,


¿L(G1)=L(G2)?

Respuesta:

•NO EXISTE algoritmo general que responda esa


pregunta.
•Por lo tanto, es un problema de decisión indecidible.
•Ese tema ya viene.
Un poco más sobre parseo

Los LLC nos interesan principalmente porque se usan.

Y para usarlos, hay que parsearlos.

¿Cuando se parsean LLC?

Respuesta: al procesar...

•Lenguajes de programación.
•Lenguajes de marcas ("ML")
•Lenguajes humanos (casi, casi)

Veamos el caso de los "ML"


HTML como GLC

El lenguaje de marcas (markup language) más


conocido: HTML.
<html>
<body>
Un ejemplo de listas anidadas:
<h4>Un ejemplo de listas
anidadas:</h4> •Cafe
<ul> •Te
<li>Cafe</li>
•Te negro
<li>Te
<ul> •Te verde
<li>Te negro</li> •Leche
<li>Te verde</li>
</ul> Ese fue un
</li> ejemplo.
<li>Leche</li>
</ul>
Ese fue un <br> ejemplo.
</body>
</html>
HTML como GLC
<ul>
Hay marcas que <li>Cafe</li>
comienzan y cierran <li>Te
entornos: <tag> ... </tag> <ul>
<li>Te negro</li>
<li>Te verde</li>
Algunos tags van solos, </ul>
como el <br>. </li>
<li>Leche</li>
</ul>
Claramente hay una Ese fue un <br>
estructura gramatical. ejemplo.
HTML como GLC
<ul>
<li>Cafe</li>
<li>Te
<ul>
<li>Te negro</li>
<li>Te verde</li>
Doc   | Elemento Doc </ul>
</li>
Elemento  Texto | <ul>Lista</ul> <li>Leche</li>
</ul>
Texto   | Caracter Texto Ese fue un <br>
ejemplo.
Caracter  <br> | a | b | ... | z | A | B...

Lista   | ElementoLista Lista


Pues HTML no exige el
ElementoLista  <li>Doc</li> | <li>Doc </li> de cierre. Pero lo
recomienda, para
simplificar el parseo.
Marcado semántico

Solían usarse los tags del html para darle formato al


texto: <i>...</i> ponía texto en itálicas, <b>...</b> lo
ponía en negritas, etc.

•En HTML más reciente, se prefiere una marcación


semántica. Por ejemplo <em>...</em> para una frase con
énfasis. Luego al desplegar el documento se puede
usar itálicas (por ejemplo) para indicar ese énfasis.

Ventajas:
•La forma (CSS) y el contenido (HTML) se
especifican por separado.
•La marcación semántica permite lectura vía
software.
Marcado semántico

<H1>Hot Cop</H1>

En el XML se <i> by Jacques Morali, Henri Belolo</i>


<ul>
generaliza la idea, <li>Producer: Jacques Morali
<li>Publisher: PolyGram Records
para todo tipo de <li>Length: 6:20

información.
<li>Written: 1978
<li>Artist: Village People
</ul>

<SONG> <?xml version="1.0"?>


<TITLE>Hot Cop</TITLE> <xsl:stylesheet xmlns:xsl="http://www.w
<COMPOSER>Jacques Morali</COMPOSER> xsl">
<COMPOSER>Henri Belolo</COMPOSER> <xsl:template match="/">
<COMPOSER>Victor Willis</COMPOSER> <html> <head><title>Song</title></h
<PRODUCER>Jacques Morali</PRODUCER> <body><xsl:value-of select="."/><
<PUBLISHER>PolyGram Records</PUBLISHER> </html>
<LENGTH>6:20</LENGTH> </xsl:template>
<YEAR>1978</YEAR> <xsl:template match="TITLE">
<ARTIST>Village People</ARTIST> <h1><xsl:value-of select="."/></h1>
</SONG> </xsl:template>
...
XML

El XML (eXtensible Markup Language) se usa mucho


para comunicar y almacenar información en la web.

•XHTML (HTML post XML)


•Feeds RSS
•Documentos (p.ej., docx o xmlx de Microsoft)
•Archivos gráficos (SVG)
•Música; matemáticas; wikipedia; transacciones
financieras; servicios web; etc etc
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<rect x="20" y="20" rx="20" ry="20" width="250"
height="100"
style="fill:red;stroke:black;stroke-width:5;opacity:0.5"/>
</svg>
XML y DTD

De esa misma flexibilidad se deduce que no es un


lenguaje; es más bien una forma de definir un lenguaje.

•El lenguaje mismo se define en el DTD (Document


Type Definition).
•Cualquiera puede inventar su XML, y publicar el
DTD. Por eso es buena costumbre incluir una
referencia al DTD en el XML.

<?xml version="1.0" standalone="no"?>


<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" version="1.1"
xmlns="http://www.w3.org/2000/svg">
<rect x="20" y="20" rx="20" ry="20" width="250"
height="100"
style="fill:red;stroke:black;stroke-width:5;opacity:0.5"/>
</svg>
DTD

Y el DTD es...
una gramática de libre contexto.

Para ser más precisos: es una GLC extendida : se


permite que al lado derecho de las producciones
aparezcan expresiones regulares.
S  (a+c)* (X+Y)
X  a*
Y  b*
No es difícil demostrar que para una GLC extendida
siempre existe una GLC "normal" equivalente. Nos
deshacemos de las ER agregando variables y
producciones.
DTD

El DTD es una gramática de libre contexto.

Forma:

<!DOCTYPE nombre-del-DTD [
lista de definiciones de elementos
]>

Definición de elemento:

<!ELEMENT nombre-del-elemento (descripción)>


DTD

La descripción es una expresión regular:

Base:

•Los nombres de otros elementos


•#PCDATA, es cualquier texto

Operadores:
| unión
, concatenación
* clausura de Kleene: 0 o más ocurrencias
+ clausura positiva: 1 o más ocurrencias
? opcional: 0 o 1 ocurrencias
DTD

Ejemplo DTD:

<!DOCTYPE PcSpecs [
<!ELEMENT PCS (PC*)>
<!ELEMENT PC (MODEL, PRICE, PROC, RAM, DISK+)>
<!ELEMENT MODEL (#PCDATA)>
<!ELEMENT PRICE (#PCDATA)>
<!ELEMENT PROC (MANF, MODEL, SPEED)>
<!ELEMENT MANF (#PCDATA)>
<!ELEMENT SPEED (#PCDATA)>
<!ELEMENT RAM (#PCDATA)>
<!ELEMENT DISK (HARDDISK | CD | DVD )>
<!ELEMENT HARDDISK (MANF, MODEL, SIZE)>
<!ELEMENT SIZE (#PCDATA)>
<!ELEMENT CD (SPEED)>
<!ELEMENT DVD (SPEED)>
]>
DTD

<PCS>
<PC>
Fragmento de un XML
<MODEL>4560</MODEL> según ese DTD
<PRICE>$2295</PRICE>
<PROCESSOR>
<MANF>Intel</MANF>
<MODEL>Pentium</MODEL>
<SPEED>4Ghz</SPEED>
</PROCESSOR>
<RAM>8192</RAM>
<DISK>
<HARDDISK>
<MANF>Maxtor</MANF>
<MODEL>Diamond</MODEL>
<SIZE>2000Gb</SIZE>
</HARDDISK>
</DISK>
<DISK><CD><SPEED>32x</SPEED></CD></DISK>
</PC>
<PC> ….. </PC>
</PCS>
XML

Moralejas:

•Las gramáticas sirven para hacer legibles


(humanamente, o automáticamente) conjuntos
de datos de casi cualquier tipo.

•Por facilidad de parseo, lo que se usa son


GLC (y rara vez hace falta más).

•Todo lenguaje decente hoy por hoy tiene una


(o más) buena librería para parsear xml.

•El XML es nuestro amigo.


Parseo y compiladores

El otro uso (más clásico, pero 100% vigente) de las


GLC: lenguajes de programación.

T o t a l = p r e c i o + i v a ; Análisis léxico
("lexer")

Total = precio + iva ;

asignación
Análisis
Expresión sintáctico
Total :=
("parser")
id + id
Análisis semántico
(chequeo de tipos, etc)
precio iva

Generación de
código
Parseo y compiladores

•Parsear un string: determinar acaso pertenece al


lenguaje de la gramática, y si es así, determinar un
árbol de derivación.

•El algoritmo CYK es demasiado lento para parsear


código, y no hay algoritmos más rápidos para GLC
arbitrarias.

 Lo que se hace es usar GLC que se puedan parsear


con algoritmos más rápidos.

Para efectos prácticos, eso se traduce en LLC


deterministas.
Parseo y compiladores

Dos aproximaciones:
Top down:
•Partimos de la raíz del árbol de derivación (S)
•Tomamos una regla de producción, e intentamos
calzarla con el input.
•Si en algún momento no podemos avanzar, nos
devolvemos por la rama y probamos otra cosa
(backtracking).
Bottom up:
•Vamos procesando las hojas, creciendo hacia la raíz.
•A medida que leemos el input, los posibles árboles
se codifican en un estado interno.
Parseo y compiladores

Dentro de bottom up, parseadores LR(1):

L: lee el input de izquierda (L) a derecha.


R: reproduce una derivación derecha (R).
1: puede "espiar" 1 símbolo hacia adelante.

LR(k): idem, pero espiando k símbolos.

¿Qué significa eso?


Parseo y compiladores

S  Tc Pila Input Acción


T  TA | A
 abaabbc shift
A  aTb | ab a baabbc shift
ab aabbc reduce
•Leemos el input de A aabbc reduce
T aabbc shift
izquierda a derecha. Ta abbc shift
•Vamos guardándolo en una Taa bbc shift
Taab bc reduce
pila (pasos "shift"). TaA bc reduce
•Si se detecta en la pila algo TaT bc shift
TaTb c reduce
que corresponde al lado TA c reduce
derecho de una producción, T c shift
se reemplaza (pasos Tc  reduce
"reduce"). S 
Parseo y compiladores

S  Tc Pila Input Acción


T  TA | A
 abaabbc shift
A  aTb | ab a baabbc shift
ab aabbc reduce
•Si desandamos lo andado, lo A aabbc reduce
T aabbc shift
que tenemos es una Ta abbc shift
derivación derecha. Taa bbc shift
Taab bc reduce
S TaA bc reduce
T TaT bc shift
A TaTb c reduce
T TA c reduce
T T c shift
A A Tc  reduce
a b a a b b c S 
Parseo y compiladores

¿Qué acción realizar en cada paso?

•Decimos que una palabra w es un "item válido


completo" si es el lado derecho de alguna producción
de la gramática ("aTb").

•Decimos que es un "item válido incompleto" si es un


prefijo de un ítem válido ("aT").

Entonces, hay dos casos en que la acción está clara.


Parseo y compiladores

 Si hay un único item válido, y está completo,


reemplazarlo (REDUCE).

 Si no hay ningún item válido completo, seguir leyendo


(SHIFT).

Casos problemáticos:

•Más de un item válido completo (conflicto R/R, dos


opciones de reduce).

•Algunos items válidos completos, otros incompletos


(conflicto S/R: ¿shift o reduce?).
Parseo y compiladores

Aquí entra la cantidad de símbolos:

Un parseador LR(k) "espía" los primeros k símbolos


del input restante, y usa eso para resolver el
conflicto.

Una gramática LR(k) es una que admite un


parseador de ese tipo: con k símbolos todo
conflicto se resuelve.

En particular, una gramática LR(0) es una en que el


conflicto jamás se produce.
Parseo y compiladores

Entonces, parser LR(k):


L: lee el input de izquierda (L) a derecha.
R: reproduce una derivación derecha (R).
k: puede "espiar" k símbolo hacia adelante.

Se puede demostrar que:


•Toda gramática LR(k), k>1, se puede convertir en
una gramática LR(1) equivalente.
•L es un LLC determinista  L admite una
gramática LR(k), para algún k.
Parseo y compiladores

Lenguajes

Lenguajes de libre contexto


(LLC)
Java, Perl,
LLC deterministas LR(1) Python, etc...
LR(0)

Lenguajes
regulares
YACC

Tiempo atrás mencionamos LEX, que a partir de una


lista de expresiones regulares y código asociado,
genera un analizador léxico.

YACC es el hermano de LEX: a partir de una


gramática y código asociado, genera un analizador
sintáctico (un parseador).

YACC: "yet another compiler compiler".


YACC

El programa generado por YACC recibirá como input


los tokens producidos por el programa generado por
LEX.

La parte que
ER + código GLC + código uno tiene que
hacer
LEX YACC

input tokens árbol Compilador (o lo


"lexer" "parser" que queramos
hacer)
YACC

•En algunos casos el código que asociamos al parsear


puede ser suficiente para nuestros fines; en esos
casos no necesitamos hacer nada más con el output
del parser.

•YACC genera un parser LALR(1): es LR(1), optimizado.

•Por lo tanto, nuestra gramática debiera ser LR(1). Si


no, habrá conflictos sin resolver.

•Se pueden especificar precedencias para resolver


conflictos (la alternativa es cambiar la gramática).
YACC

•YACC informa sobre los conflictos (R/R, S/R); eso


ayuda a evitarlos!

•En ayudantía verán más sobre LEX y YACC; la tarea


2 consiste en usarlos.

•http://www.giaa.inf.uc3m.es/docencia/II/PL2/herra
mientas/YACC.pdf
•http://epaperpress.com/lexandyacc/
•http://dinosaur.compilertools.net/

Potrebbero piacerti anche