Parsing

17
1 Parsing Un parser podría ser definido como un programa que analiza una porción de texto para determinar su estructura lógica: la fase de parsing en un compilador toma el texto de un programa y produce un arbol sintáctico que representa la estructura del programa. Diferentes métodos para construir parsers, aquí presentaremos una metodología que ha sido ampliamente aceptada para ser usada en un contexto de programación funcional- perezosa.

description

Parsing. Un parser podría ser definido como un programa que analiza una porción de texto para determinar su estructura lógica: la fase de parsing en un compilador toma el texto de un programa y produce un arbol sintáctico que representa la estructura del programa. - PowerPoint PPT Presentation

Transcript of Parsing

Page 1: Parsing

1

ParsingUn parser podría ser definido como un programa que analiza una porción de texto para determinar su estructura lógica: la fase de parsing en un compilador toma el texto de un programa y produce un arbol sintáctico que representa la estructura del programa.

Diferentes métodos para construir parsers, aquí presentaremos una metodología que ha sido ampliamente aceptada para ser usada en un contexto de programación funcional-perezosa.

Parsers son modelados como funciones. Parsers complejos son obtenidos a partir de otros más simples haciendo un amplio uso de alto orden.

Page 2: Parsing

2

Parsing (2)Definiremos funciones de alto orden para representar secuencia, alternancia y repetición. Así, el código de un parser se asemejará notablementa a la notación BNF de la gramática que reconocen.

Parsers en este estilo son muy fáciles de construir, simples de entender y modificar. Esta metodología se conoce con el nombre de combinator parsing.

Características:• Se pueden reconocer gramáticas ambiguas• Backtracking• Acciones semánticas

Page 3: Parsing

3

El tipo ParserPensemos un parser como una función del siguiente tipo

type Parser = String -> Tree

Problema: no provee una forma natural para secuenciar parsers. En un parser para expresiones aritméticas uno quisiera buscar primero un número, luego un operador y luego otro número. Cada uno de estos procesos consume parte de la entrada.

Una buena idea sería entonces refinar el tipo Parser de forma que el string de entrada no consumido sea retornado como parte del resultado:

type Parser = String -> (Tree, String)

Page 4: Parsing

4

El tipo Parser (2)Otro aspecto a considerar: puede pasar que un parser falle tratando de reconocer un string de entrada, esto no es en principio un error. En una expresión aritmética podríamos querer buscar un símbolo de operación o un paréntesis que abre. En general, un parser puede encontrar diferentes formas en que una porción inicial de la entrada puede ser estructurada como un Tree.

Falla, entonces, corresponde al caso particular en que no existen resultados de parsing.

Refinamos entonces el tipo Parser de tal forma de poder retornar una lista de pares:

type Parser = String -> [(Tree, String)]

Page 5: Parsing

5

El tipo Parser (3)

El último refinamiento que efectuaremos está motivado por el hecho de que diferentes parsers pueden tanto operar sobre distintos tipos de entrada así como retornar diferentes clases de arboles sintácticos.

Entonces abstraemos en los tipos Char y Tree, para definir a un parser como una función del siguiente tipo:

type Parser a b = [a] -> [(b , [a])]

Por ejemplo, un parser para expresiones aritméticas podría ser un objeto de tipo Parser Char Exp.

Page 6: Parsing

6

Parsers primitivosEstos son los bloques básicos de construcción de combinadores de parsing.

El primero de ellos corresponde al símbolo de la notación BNFel que denota el string vacío.

El parser succeed siempre es exitoso, y no consumeninguna porción del string de entrada:

succeed :: b Parser a bsucceed v inp = [( v, inp)]

Ya que el resultado de succeed no depende de su entrada, el tipo de estos valores debe ser predeterminado, y es incluído como un parámetro extra.

Page 7: Parsing

7

Parsers primitivos (2)Mientras que succeed nunca falla, el parser fail siempre lo hace, independientemente de cual sea la entrada:

fail :: Parser a bfail inp = []

La siguiente función nos permitirá construir parsers que reconocen un único símbolo. En vez de enumerar los símbolos aceptables, se provee un predicado que determina si el símbolo pertenece a este conjunto:

satisfy :: (a bool) Parser a asatisfy p [] = fail []satisfy p (x:xs) | p x = succeed x xs | otherwise = fail xs

Page 8: Parsing

8

Parsers primitivos (3)

Usando satisfy podemos definir, como dicho anteriormente, parsers que aceptan un símbolo específico:

literal :: a Parser a aliteral x = satisfy (= x)

Por ejemplo, si aplicamos el parser (literal ‘3’) a el string “345”, esto nos da como resultado la lista [(‘3’, “45”)].

Page 9: Parsing

9

Combinadores

En notación BNF gramáticas complejas son definidas a partir de otras de menor complejidad usando, por ejemplo, | , con el cual se denota alternancia y juxtaposición para denotar secuenciación.

Definiremos funciones de alto orden que se corresponderán con los operadores arriba mencionados. Ya que estas funciones combinan parsers para construir otros parsers las llamaremos combinadores de parsing.

Page 10: Parsing

10

Combinadores (2)El combinador `alt` corresponde a alternancia en BNF:

infixr 4 `alt`(alt) :: Parser a b Parser a b Parser a b p1 `alt` p2 = \inp -> p1 inp ++ p2 inp

El parser (p1 `alt` p2) reconoce todo lo que reconoce p1 o lo que reconoce p2 (inclusivamente).

Se puede verificar fácilmente que:

(fail `alt` p) = (p `alt` fail) = p(p `alt` q) `alt` r = p `alt`(q `alt` r) (heredada de ++)

Page 11: Parsing

11

Combinadores (3)El combinador `seq` correponde a secuencia en BNF:

infixr 6 `seq`(seq) :: Parser a b Parser a c Parser a (b,c)p1 `seq` p2 = \ inp -> [((v1,v2), out2) | (v1,out1) p1 inp; (v2,out2) p2 out1]

El parser (p1 `seq` p2) reconoce todo lo que reconocen p1 y p2 en secuencia. Ya que el primer parser puede terminar con varios resultados, cada uno con con su correpondiente string de entrada no consumido, el segundo parser debe ser aplicado a cada uno de estos resultados. Dos resultados son producidos, uno por cada parser.

Por ejemplo, si aplicamos literal‘a’ `seq` literal ‘b’ a la entrada “abcd” esto da como resultado [((‘a’, ‘b’), “cd”)].

Page 12: Parsing

12

Manipulación de valoresUna porción del resultado de un parser es un valor. El combinador using nos permite manipular valores, donde la creación de un árbol sintáctico es una de las aplicaciones más comunes:

(using) :: Parser a b (b c) Parser a cp `using` q = \inp -> [(f v, out) |(v,out) p inp]

El parser (p `using` f) se comporta como el parser p salvo que la función f es aplicada a cada uno de los valores de su resultado.

Aunque `using` no tiene contraparte en notación BNF tiene mucho en común con el operador {...} de Yacc.

Page 13: Parsing

13

Ignorando valores y RepeticiónDos distintas versiones de `seq` que son muy útiles son `getl` and `getr`, las que ignoran uno de los valores retornados por los sub-parsers:infixr 8 `getl` , `getr`(getl) :: Parser a b Parser a c Parser a bp1 `getl` p2 = p1 `seq` p2 `using` fst

(getr) :: Parser a b Parser a c Parser a cp1 `getl` p2 = p1 `seq` p2 `using` snd

El combinador many toma como argumento un parser y lo aplica repetidamente hasta que falla:many :: Parser a b Parser a [b]many p = p `seq` many p `using` (\(x,xs) -> x : xs) `alt` succeed []some p = p `seq` many p `using` cons

Page 14: Parsing

14

Combinadores de parsing monádicos

Un combinador que ha revelado ser muy útil y que no hemos presentado es `into`, el que permite no sólo secuenciar dos parsers sino que además puede hacer uso del resultado del primer parser en la definición del subsecuente parser:

infixr 6 `into`(into) :: Parser a b ( b Parser a c) Parser a cp1 `into` p2 = \ inp -> [(v2, out2 | (v1,out1) p1 inp ; (v2,out2) p2 v1 out1]

p1 `seq` p2 = p1 `into` \v -> p2 `using` \w -> (v,w)p `using` f = p `into` \v -> succeed f

Pero...

Page 15: Parsing

15

Combinadores ... (2)Un combinador más interesante de definir es `ap`, el que se puede entender como aplicación, pero de parsers:

infixr 6 `ap`(ap) :: Parser a (b c) Parser a b Parser a cp1 `ap` p2 = p1 into \f -> p2 into \x -> succeed (f x)

Nota: `into` y succeed se pueden entender como >>=y returnAhora es posible, entonces, definir a many como un combinador monádico:many p = succeed (:) `ap` p `ap` many p `alt` succeed []

infixr 6 `ap`(chk) :: Parser a b Parser a c Parser a bp1 `chk` p2 = p1 `into` \v -> p2 \ _ -> succeed v

Page 16: Parsing

16

Una versión de la mónada Parsernewtype Parser a b = MkP ([a] [ (b , [a]) ])

apply :: Parser a b [a] [ (b , [a]) ]apply (MkP f) inp = f inp

applyParser :: Parser a b [a] aapplyParser = fst . head . apply

La función applyParser retorna el primer componente del primer elemento en una lista de árboles sintácticos.

instance Monad (Parser a) where return x = MkP f where f = \inp -> [(x,inp)] p >>= q = MkP f where f = \inp -> [(v2, out2 | (v1,out1) apply p inp ; (v2,out2) apply (q v1) out1]

Page 17: Parsing

17

Combinando parsers con doHabiendo declarado (Parser a) como una mónada, ahora podemos combinar parsers usando notación do:item :: Parser a aitem = MkP f where f [] = [] f (x:xs) = [(x,xs)]

fail :: Parser a bfail = MkP f where f inp = []

satisfy :: (a Bool) Parser a asatisfy p = do c item if p c then return c else fail

digit :: Parser Char Intdigit = do d satisfy isDigit return (ord d - ord ‘0’) }