Parsec

Escrevendo parsers com Haskell

marcelocamargo@linuxmail.org

github.com/haskellcamargo

About.me

  • Systems analyst
  • Translator
  • PL researcher
  • Developer of Quack and Capybara programming languages 

TOC

λ. O que é um parser?

λ. Que tal o Parsec?

λ. Parser combinators × parser generators

λ. Referências & recomendações

O que é
um
parser?

do latim...

Pars (ōrātiōnis)

Parte (de uma linguagem)

λ. O que é um parser

  • Base de um compilador

  • Análise sintática baseada em uma gramática

  • Construção de linguagens

λ. O que é um parser

LEXER

λ. O que é um parser ─ quebrando o front-end de um compilador

var x = 10 + 20 * 30;
[           var]
[IDENTIFIER x  ]
[           =  ]
[INTEGER    10 ]
[           +  ]
[INTEGER    20 ]
[           *  ]
[INTEGER    30 ]
[           ;  ]
  • Divide o texto em um conjunto de tokens
  • Classifica-os com tags e talvez valores
  • “Alimenta” o parser

PARSER

λ. O que é um parser ─ quebrando o front-end de um compilador

var x = 10 + 20 * 30;
var-decl   := "var" IDENTIFIER "=" expression;
expression := expression ("+"|"*") expression
            | INTEGER;
  • Consome tokens
  • Possui uma gramática
  • Pode gerar uma árvore sintática

ÁRVORE SINTÁTICA

λ. O que é um parser ─ quebrando o front-end de um compilador

var x = 10 + 20 * 30;
  ____ = ____
 /           \
x         ___ + ___
         /         \
        10       __ * __
                /       \
               20       30
  • Árvore representa o programa
  • Análise semântica
  • Pode gerar código

GRAMÁTICA FORMAL

λ. O que é um parser ─ projetando linguagens

  • Definição formal de uma linguagem
  • Conjunto de regras e produções que a validam

Parser generators

Parser combinators

×

O que é
um parser
generator?

O que é um parser generator?

λ. Parser generators × parser combinators ─ parser generators

  • Ferramenta que recebe uma linguagem formal e dá o código fonte de um parser como saída
  • Grande parte das linguagens modernas são escritas usando parser generators

Exemplos de parser generators

λ. Parser generators × parser combinators ─ parser generators

  • ANTLR (Groovy, Jython)
  • Bison (Ruby, PHP, Go, Bash, MySQL)

Bison grammar for PHP integers

λ. Parser generators × parser combinators ─ parser generators

O que é
um parser
combinator?

O que é um parser combinator?

λ. Parser generators × parser combinators ─ parser combinators

  • Função de mais alta ordem que aceita vários parsers de entrada e dá outro como saída
  • Parsers são compostos sempre através de outros parsers, tudo de maneira combinada

O que é um parser combinator?

λ. Parser generators × parser combinators ─ parser combinators

  • Parser combinators são legíveis, modulares, bem estruturados e fáceis de manter
  • Não são dependentes de um lexer, em sua maioria

Exemplos de parser combinators

λ. Parser generators × parser combinators ─ parser combinators

  • Parsec (for Haskell)
  • Parsimmon (for JavaScript)

Parser generators rodam compile-time

λ. Parser generators × parser combinators

Parser combinators rodam com o seu programa

Que tal
o
Parsec?

Parsec é um

parser
combinator!

Que tal o Parsec?

λ. Que tal o Parsec?

  • Poderoso combinador monádico de parsers para Haskell
  • Analisa gramáticas sensíveis ao contexto, finitas ou infinitas

Que tal o Parsec?

λ. Que tal o Parsec?

  • Combine parsers e crie expressões maiores
  • Existem ports para JS, Python, OCaml, Java, F# e mais uma dezena de linguagens
cabal install parsec

λ. Que tal o Parsec?

stack install parsec

Escrevendo
um parser de
JSON

module JSON where
{-# LANGUAGE NoMonomorphismRestriction #-}

-- Precisamos para mostrar o JSON
import Data.List (foldl', intersperse)
-- Analisar sequências de escape
import Data.Char (chr, ord)
-- Representar arrays associativos
import qualified Data.Map as Map

import Text.Parsec
import Text.Parsec.Char

λ. Que tal o Parsec? ─ Construindo um parser de JSON

Import stuff...

-- Nossa estrutura para representar o JSON
data JSON = JsonNum Double
          | JsonStr String
          | JsonArr [JSON]
          | JsonAA (Map.Map String JSON)
          | JsonBool Bool
          | JsonNull
          deriving (Eq, Ord)

λ. Que tal o Parsec? ─ Construindo um parser de JSON

Representamos a gramática usando ADTs

-- Transformar um JSON em uma string para exibição
instance Show JSON where
    showsPrec _ = showsJSON where
        commaJoin                =
            foldl' (.) id . intersperse (", " ++)

        showsAssoc (field, val)  =
            shows field . (": " ++) . shows val

        showsJSON (JsonNum num)  =
            shows num

        showsJSON (JsonStr str)  =
            shows str

        showsJSON (JsonArr jarr) =
            ('[' :) .
            commaJoin (map shows jarr) .
            (']' :)

        showsJSON (JsonAA aa)    =
            ('{' :) .
            commaJoin (map showsAssoc $ Map.toList aa) .
            ('}' :)

        showsJSON (JsonBool tf)  =
            if tf then ("true" ++) else ("false" ++)

        showsJSON JsonNull       =
            ("null" ++)

λ. Que tal o Parsec? ─ Construindo um parser de JSON

Criamos uma função para exibição do JSON

-- A base do parser
jsonParser = do
    spaces -- coma os espaços iniciais
    -- Analise as possibilidades
    json <- jsonPNum
        <|> jsonPStr
        <|> jsonPBool
        <|> jsonPNull
        <|> jsonPArr
        <|> jsonPAA
        <?> "value" -- esperava...
    spaces
    return json

λ. Que tal o Parsec? ─ Construindo um parser de JSON

Definimos as possibilidades de valores

jsonPNull = try (string "null")
         >> return JsonNull

λ. Que tal o Parsec? ─ Construindo um parser de JSON

JSON pode conter valores nulos, certo?

jsonPBool = fmap JsonBool
    $ (try (string "true") >> return True)
    <|> (try (string "false") >> return False)

λ. Que tal o Parsec? ─ Construindo um parser de JSON

E booleanos!

jsonPNum = do
    -- Uma função que pode negar nosso resultado
    doNeg <- (char '-' >> return negate) <|> return id
    -- Todo número pode...
    realPart <- realP
    fracPart <- fracP
    expPart  <- expP

    return $ JsonNum $ doNeg $ (realPart + fracPart) * 10 ** expPart
        -- Parte real do número. Deve existir
  where realP = (char '0' >> return 0) <|> (do
                -- 0 ou entre 1 a 9
                firstDigit <- satisfy (\ ch -> '1' <= ch && ch <= '9')
                -- Depois, quaisquer digitos
                restDigits <- many digit
                return $ read $ firstDigit : restDigits
            )
        fracP = (do
                char '.'
                digits <- many1 digit
                return $ read $ "0." ++ digits
            )
            -- Se não há um (.), o padrão é 0.
            <|> return 0
        expP = (do
            oneOf "eE"
            -- É opcional a notação exponencial
            doNeg <- (char '-' >> return negate)
                <|> (char '+' >> return id)
                <|> return id
            digits <- many1 digit
            return $ doNeg $ read digits
            )
            -- 0 é padrão para a notação exponencial
            <|> return 0

λ. Que tal o Parsec? ─ Construindo um parser de JSON

Números são um pouco mais complexos...

jsonStr = do
    char '\"'
    strChar `manyTill` (char '\"')
        -- Lista de substituição após \
  where subMap = Map.fromList
        [ ('\"', '\"')
        , ('\\', '\\')
        , ('/', '/')
        , ('n', '\n')
        , ('r', '\r')
        , ('f', '\f')
        , ('t', '\t')
        , ('b', '\b')
        ]
    -- Análise unicode
    unicode = do
        -- 4 dígitos hexadecimais
        code <- count 4 hexDigit
        return $ chr $ read $ "0x" ++ code

    -- Finalmente analisar o \
    strChar = (do
        char '\\'
        ch <- anyChar
        -- Buscar caso de substituição
        case Map.lookup ch subMap of
            Just newCh -> return newCh
            Nothing -> do
                if ch == 'u' -- It might yet be unicode
                  then unicode
                  else fail "expecting escape sequence"
        )
        -- Não há sequência? Retorne o próprio caractere
        <|> anyChar

-- Guarde o valor do parsing numa string JSON
jsonPStr = fmap JsonStr jsonStr

λ. Que tal o Parsec? ─ Construindo um parser de JSON

Podemos representar strings com escape

-- Somente listas de valores JSON separados por
-- vírgulas!
jsonPArr = do
    char '['
    jarr <- jsonParser `sepBy` (char ',')
    char ']'
    return $ JsonArr $ jarr

λ. Que tal o Parsec? ─ Construindo um parser de JSON

Que tal representarmos arrays simples?

jsonPAA = do
    char '{'
    jAA <- assocP `sepBy` char ','
    char '}'
    return $ JsonAA $ Map.fromList $ jAA
  where assocP = do
            spaces
            label <- jsonStr
            spaces
            char ':'
            json <- jsonParser
            return (label, json)

λ. Que tal o Parsec? ─ Construindo um parser de JSON

Arrays associativos são tão simples quanto!

-- Podemos até fazer um mini-REPL!
main = do
    line <- getLine
    case parse jsonParser "" line of
        Right json -> do
            print json
            main
        Left err -> print err

λ. Que tal o Parsec? ─ Construindo um parser de JSON

Sério? Um parser de JSON em 100 linhas?

E como usamos?

Referências

&
Recomendações

λ. Referências & recomendações

slides.com/marcelocamargo/parsec

  • Compilers: principles, techniques and tools
  • Let's build a compiler!

OBRIGADO!

Questions or Concerns?

Parsec: Escrevendo parsers com Haskell

By Marcelo Camargo

Parsec: Escrevendo parsers com Haskell

  • 1,822