Parser combinator - Parser combinator

Em programação de computador , um combinador de analisador é uma função de ordem superior que aceita vários analisadores como entrada e retorna um novo analisador como sua saída. Nesse contexto, um analisador é uma função que aceita strings como entrada e retorna alguma estrutura como saída, normalmente uma árvore de análise ou um conjunto de índices que representam locais na string onde a análise foi interrompida com êxito. Os combinadores de analisador permitem uma estratégia de análise descendente recursiva que facilita a construção e teste modular por partes. Essa técnica de análise é chamada de análise combinatória .

Analisadores que usam combinadores têm sido usados ​​extensivamente na prototipagem de compiladores e processadores para linguagens de domínio específico , como interfaces de linguagem natural para bancos de dados, onde ações semânticas complexas e variadas estão intimamente integradas ao processamento sintático. Em 1989, Richard Frost e John Launchbury demonstraram o uso de combinadores de analisador para construir intérpretes de linguagem natural . Graham Hutton também usou funções de ordem superior para análise básica em 1992. SD Swierstra também exibiu os aspectos práticos dos combinadores de analisador em 2001. Em 2008, Frost, Hafiz e Callaghan descreveram um conjunto de combinadores de analisador em Haskell que resolvem o problema de longa data de acomodar a recursão à esquerda e funcionar como uma ferramenta de análise completa de cima para baixo em tempo e espaço polinomial.

Ideia básica

Em qualquer linguagem de programação que tenha funções de primeira classe , os combinadores de analisador podem ser usados ​​para combinar analisadores básicos para construir analisadores para regras mais complexas. Por exemplo, uma regra de produção de uma gramática livre de contexto (CFG) pode ter uma ou mais alternativas e cada alternativa pode consistir em uma sequência de não terminal (s) e / ou terminal (es), ou a alternativa pode consistir em um único não terminal ou terminal ou a string vazia. Se um analisador simples estiver disponível para cada uma dessas alternativas, um combinador de analisador pode ser usado para combinar cada um desses analisadores, retornando um novo analisador que pode reconhecer qualquer uma ou todas as alternativas.

Em linguagens que suportam sobrecarga de operador , um combinador de analisador pode assumir a forma de um operador infixo , usado para colar diferentes analisadores para formar uma regra completa. Os combinadores de analisador permitem, portanto, que os analisadores sejam definidos em um estilo embutido, em código que é semelhante em estrutura às regras da gramática formal. Como tal, as implementações podem ser consideradas especificações executáveis ​​com todas as vantagens associadas. (Notavelmente: legibilidade)

Os combinadores

Para manter a discussão relativamente direta, discutimos combinadores de analisador em termos de reconhecedores apenas. Se a string de entrada for longa #inpute seus membros forem acessados ​​por meio de um índice j, um reconhecedor é um analisador que retorna, como saída, um conjunto de índices que representam as posições nas quais o analisador concluiu com sucesso o reconhecimento de uma sequência de tokens que começou na posição j. Um conjunto de resultados vazio indica que o reconhecedor falhou em reconhecer qualquer sequência começando no índice j. Um conjunto de resultados não vazio indica que o reconhecedor termina em posições diferentes com sucesso.

  • O emptyreconhecedor reconhece a string vazia. Este analisador sempre é bem-sucedido, retornando um conjunto de singleton contendo a posição atual:
  • Um reconhecedor reconhece o terminal . Se o token na posição na string de entrada for , este analisador retornará um conjunto de singleton contendo ; caso contrário, ele retorna o conjunto vazio.term xxjxj + 1

Observe que pode haver várias maneiras distintas de analisar uma string ao terminar no mesmo índice: isso indica uma gramática ambígua . Os reconhecedores simples não reconhecem essas ambigüidades; cada índice de acabamento possível é listado apenas uma vez no conjunto de resultados. Para um conjunto de resultados mais completo, um objeto mais complicado, como uma árvore de análise, deve ser retornado.

Seguindo as definições de dois reconhecedores básicos pe q, podemos definir dois combinadores de analisador principal para alternativa e sequenciamento:

  • O combinador de analisador 'alternativo', ⊕, aplica ambos os reconhecedores na mesma posição de entrada je soma os resultados retornados por ambos os reconhecedores, que eventualmente são retornados como o resultado final. É usado como um operador infixo entre pe da qseguinte forma:
  • O sequenciamento dos reconhecedores é feito com o combinador do analisador ⊛. Como ⊕, é usado como um operador infixo entre pe q. Mas ele aplica o primeiro reconhecedor pà posição de entrada je, se houver algum resultado bem-sucedido dessa aplicação, o segundo reconhecedor qé aplicado a cada elemento do conjunto de resultados retornado pelo primeiro reconhecedor. ⊛ em última análise, retorna a união dessas aplicações de q.

Exemplos

Considere um altamente ambígua gramática livre de contexto , s ::= ‘x’ s s | ε. Usando os combinadores definidos anteriormente, podemos definir modularmente as notações executáveis ​​dessa gramática em uma linguagem funcional moderna (por exemplo, Haskell ) como s = term ‘x’ <*> s <*> s <+> empty. Quando o reconhecedor sé aplicado a uma sequência de entrada xxxxxna posição 1, de acordo com as definições acima, ele retornaria um conjunto de resultados {5,4,3,2}.

Deficiências e soluções

Combinadores analisador, como todos os analisador sintático descendente recursivo , não se limitam às gramáticas livres de contexto e, portanto, não fazem pesquisa global para ambiguidades na LL ( k ) parsing Primeira k e siga k conjuntos. Assim, as ambigüidades não são conhecidas até o tempo de execução se e até que a entrada as dispare. Nesses casos, o analisador descendente recursivo pode usar como padrão (talvez desconhecido para o designer da gramática) um dos possíveis caminhos ambíguos, resultando em confusão semântica (aliasing) no uso da linguagem. Isso leva a bugs por usuários de linguagens de programação ambíguas, que não são relatadas em tempo de compilação e que são introduzidas não por erro humano, mas por gramática ambígua. A única solução que elimina esses bugs é remover as ambigüidades e usar uma gramática livre de contexto.

As implementações simples de combinadores de analisador têm algumas deficiências, que são comuns na análise de cima para baixo. A análise combinatória ingênua requer tempo e espaço exponencial ao analisar uma gramática livre de contexto ambígua. Em 1996, Frost e Szydlowski demonstraram como a memoização pode ser usada com combinadores de analisador para reduzir a complexidade do tempo para polinômios. Mais tarde, Frost usou mônadas para construir os combinadores para o encadeamento sistemático e correto da tabela de memorandos ao longo do cálculo.

Como qualquer análise sintática descendente recursiva de cima para baixo , os combinadores de analisadores convencionais (como os combinadores descritos acima) não terminarão durante o processamento de uma gramática recursiva à esquerda (por exemplo s ::= s <*> term ‘x’|empty). Um algoritmo de reconhecimento que acomoda gramáticas ambíguas com regras recursivas à esquerda diretas é descrito por Frost e Hafiz em 2006. O algoritmo restringe a análise recursiva à esquerda sempre crescente, impondo restrições de profundidade. Esse algoritmo foi estendido a um algoritmo de análise completa para acomodar recursão à esquerda indireta e direta em tempo polinomial e para gerar representações compactas de tamanho polinomial do número potencialmente exponencial de árvores de análise para gramáticas altamente ambíguas de Frost, Hafiz e Callaghan em 2007. Este algoritmo estendido acomoda recursão esquerda indireta comparando seu 'contexto computado' com o 'contexto atual'. Os mesmos autores também descreveram a implementação de um conjunto de combinadores de analisadores escritos na linguagem de programação Haskell com base no mesmo algoritmo.

Notas

Referências

  • Burge, William H. (1975). Técnicas de programação recursiva . A série de programação de sistemas. Addison-Wesley. ISBN 978-0201144505.
  • Frost, Richard; Launchbury, John (1989). "Construindo intérpretes de linguagem natural em uma linguagem funcional preguiçosa" (PDF) . The Computer Journal . Edição especial sobre programação funcional preguiçosa. 32 (2): 108–121. doi : 10.1093 / comjnl / 32.2.108 . Arquivado do original em 06/06/2013.CS1 maint: bot: status do URL original desconhecido ( link )
  • Frost, Richard A .; Szydlowski, Barbara (1996). "Memoizing Purely Functional Top-Down Backtracking Language Processors" (PDF) . Sci. Comput. Programa . 27 (3): 263–288. doi : 10.1016 / 0167-6423 (96) 00014-7 .
  • Frost, Richard A. (2003). Memoização monádica para redução de pesquisa preservando a correção (PDF) . Proceedings of the 16th Canadian Society for Computational Studies of Intelligence Conference on Advances in Artificial Intelligence (AI'03) . pp. 66–80. ISBN 978-3-540-40300-5.
  • Frost, Richard A .; Hafiz, Rahmatullah (2006). "Um novo algoritmo de análise de cima para baixo para acomodar ambigüidade e recursão à esquerda em tempo polinomial" (PDF) . Avisos ACM SIGPLAN . 41 (5): 46–54. doi : 10.1145 / 1149982.1149988 .
  • Frost, Richard A .; Hafiz, Rahmatullah; Callaghan, Paul (2007). "Análise modular e eficiente de cima para baixo para gramáticas recursivas à esquerda ambíguas". Proceedings of the 10th International Workshop on Parsing Technologies (IWPT), ACL-SIGPARSE : 109-120. CiteSeerX  10.1.1.97.8915 .
  • Frost, Richard A .; Hafiz, Rahmatullah; Callaghan, Paul (2008). Combinadores de analisador para gramáticas recursivas à esquerda ambíguas . Anais do 10º Simpósio Internacional sobre Aspectos Práticos das Línguas Declarativas (PADL) . ACM-SIGPLAN. 4902 . pp. 167–181. CiteSeerX  10.1.1.89.2132 . doi : 10.1007 / 978-3-540-77442-6_12 . ISBN 978-3-540-77441-9.
  • Hutton, Graham (1992). "Funções de ordem superior para análise". Journal of Functional Programming . 2 (3): 323–343. CiteSeerX  10.1.1.34.1287 . doi : 10.1017 / s0956796800000411 .
  • Okasaki, Chris (1998). "Mesmo funções de ordem superior para análise ou Por que alguém iria querer usar uma função de sexta ordem?". Journal of Functional Programming . 8 (2): 195–199. doi : 10.1017 / S0956796898003001 .
  • Swierstra, S. Doaitse (2001). "Analisadores combinadores: de brinquedos a ferramentas" (PDF) . Notas Eletrônicas em Ciência da Computação Teórica . 41 : 38–59. doi : 10.1016 / S1571-0661 (05) 80545-6 .
  • Wadler, Philip (1985). Como substituir a falha por uma lista de sucessos - Um método para tratamento de exceções, retrocesso e correspondência de padrões em linguagens funcionais preguiçosas . Proceedings of a Conference on Functional Programming Languages ​​and Computer Architecture . Notas de aula em Ciência da Computação. 201 . pp. 113–128. doi : 10.1007 / 3-540-15975-4_33 . ISBN 978-0-387-15975-1.

links externos