1 / 17

Parsing

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.

zudora
Download Presentation

Parsing

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

E N D

Presentation Transcript


  1. 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. Parsers son modelados como funciones. Parsers complejos son obtenidos a partir de otros más simples haciendo un amplio uso de alto orden.

  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

  3. El tipo Parser Pensemos 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)

  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)]

  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 Chary 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 tipoParser Char Exp.

  6. Parsers primitivos Estos son los bloques básicos de construcción de combinadores de parsing. El primero de ellos corresponde al símbolo  de la notación BNF, el 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 b succeed 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.

  7. Parsers primitivos (2) Mientras que succeed nunca falla, el parser fail siempre lo hace, independientemente de cual sea la entrada: fail :: Parser a b fail 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 a satisfy p [] = fail [] satisfy p (x:xs) | p x = succeed x xs | otherwise = fail xs

  8. Parsers primitivos (3) Usando satisfy podemos definir, como dicho anteriormente, parsers que aceptan un símbolo específico: literal :: a  Parser a a literal x = satisfy (= x) Por ejemplo, si aplicamos el parser (literal ‘3’) a el string “345”, esto nos da como resultado la lista [(‘3’, “45”)].

  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.

  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 ++)

  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”)].

  12. Manipulación de valores Una 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 c p `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.

  13. Ignorando valores y Repetición Dos 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 b p1 `getl` p2 = p1 `seq` p2 `using` fst (getr) :: Parser a b  Parser a c  Parser a c p1 `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

  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 c p1 `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...

  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 c p1 `ap` p2 = p1 into \f -> p2 into \x -> succeed (f x) Nota: `into` y succeed se pueden entender como >>=y return Ahora 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 b p1 `chk` p2 = p1 `into` \v -> p2 \ _ -> succeed v

  16. Una versión de la mónada Parser newtype 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]  a applyParser = 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]

  17. Combinando parsers con do Habiendo declarado (Parser a) como una mónada, ahora podemos combinar parsers usando notación do: item :: Parser a a item = MkP f where f [] = [] f (x:xs) = [(x,xs)] fail :: Parser a b fail = MkP f where f inp = [] satisfy :: (a  Bool)  Parser a a satisfy p = do c  item if p c then return c else fail digit :: Parser Char Int digit = do d  satisfy isDigit return (ord d - ord ‘0’) }

More Related