Qual é o procedimento seguido ao escrever um lexer com base em uma gramática?


12

Ao ler uma resposta à pergunta de esclarecimento sobre gramáticas, Lexers e Parsers , a resposta afirmou que:

[...] uma gramática BNF contém todas as regras necessárias para análise e análise lexical.

Isso me pareceu um pouco estranho, porque até agora, eu sempre pensei que um lexer não era baseado em uma gramática, enquanto um analisador era fortemente baseado em um. Cheguei a essa conclusão depois de ler várias postagens no blog sobre escrever lexers, e nunca alguém usando 1 EBNF / BNF como base para o design.

Se os lexers, assim como os analisadores, são baseados em uma gramática EBNF / BNF, como alguém criaria um lexer usando esse método? Ou seja, como eu construiria um lexer usando uma gramática EBNF / BNF?

Eu já vi muitas, muitas postagens que tratam de escrever um analisador usando EBNF / BNF como guia ou planta, mas não encontrei nenhuma até agora que mostre o equivalente ao design de lexer.

Por exemplo, considere a seguinte gramática:

input = digit| string ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"', { all characters - '"' }, '"' ;
all characters = ? all visible characters ? ;

Como alguém criaria um lexer baseado na gramática? Eu poderia imaginar como um analisador poderia ser escrito a partir dessa gramática, mas não entendi o conceito de fazer o mesmo com um lexer.

Existem certas regras ou lógica usadas para realizar uma tarefa como essa, como na escrita de um analisador? Francamente, estou começando a me perguntar se os designs de lexer usam uma gramática EBNF / BNF, e muito menos se baseiam em uma.


1 Forma Backus – Naur estendida e Forma Backus – Naur

Respostas:


17

Lexers são apenas analisadores simples que são usados ​​como uma otimização de desempenho para o analisador principal. Se temos um lexer, o lexer e o analisador trabalham juntos para descrever o idioma completo. Os analisadores que não possuem um estágio de lexing separado às vezes são chamados de "sem scanner".

Sem lexers, o analisador teria que operar caractere por caractere. Como o analisador precisa armazenar metadados sobre cada item de entrada e pode precisar pré-calcular tabelas para cada estado do item de entrada, isso resultaria em consumo de memória inaceitável para tamanhos de entrada grandes. Em particular, não precisamos de um nó separado por caractere na árvore de sintaxe abstrata.

Como o texto caractere por caractere é bastante ambíguo, isso também resultaria em muito mais ambiguidade que é chata de manusear. Imagine uma regra R → identifier | "for " identifier. onde identificador é composto de letras ASCII. Se eu quiser evitar ambiguidade, agora preciso de um cabeçote de quatro caracteres para determinar qual alternativa deve ser escolhida. Com um lexer, o analisador apenas precisa verificar se possui um token IDENTIFIER ou FOR - um visualizador de 1 token.

Gramáticas de dois níveis.

Os Lexers trabalham traduzindo o alfabeto de entrada para um alfabeto mais conveniente.

Um analisador sem scanner descreve uma gramática (N, Σ, P, S) em que os não terminais N são os lados esquerdos das regras da gramática, o alfabeto Σ é, por exemplo, caracteres ASCII, as produções P são as regras da gramática , e o símbolo inicial S é a regra de nível superior do analisador.

O lexer agora define um alfabeto de tokens a, b, c,…. Isso permite que o analisador principal use esses tokens como alfabeto: Σ = {a, b, c,…}. Para o lexer, esses tokens são não terminais e a regra de início S L é S L → ε | um S | b S | c S | …, Ou seja: qualquer sequência de tokens. As regras da gramática lexer são todas necessárias para produzir esses tokens.

A vantagem de desempenho vem da expressão das regras do lexer como uma linguagem regular . Eles podem ser analisados ​​com muito mais eficiência do que as linguagens sem contexto. Em particular, idiomas regulares podem ser reconhecidos em O (n) espaço e O (n) tempo. Na prática, um gerador de código pode transformar esse lexer em tabelas de salto altamente eficientes.

Extraindo tokens da sua gramática.

Para tocar no seu exemplo: as regras digite stringsão expressas em um nível de caracter por caracter. Nós poderíamos usá-los como tokens. O restante da gramática permanece intacto. Aqui está a gramática lexer, escrita como uma gramática linear à direita para deixar claro que é regular:

digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
string = '"' , string-rest ;
string-rest = '"' | STRING-CHAR, string-rest ;
STRING-CHAR = ? all visible characters ? - '"' ;

Mas como é regular, normalmente usamos expressões regulares para expressar a sintaxe do token. Aqui estão as definições de token acima como regexes, escritas usando a sintaxe de exclusão da classe de caracteres .NET e as classes POSIX:

digit ~ [0-9]
string ~ "[[:print:]-["]]*"

A gramática do analisador principal contém as regras restantes não tratadas pelo lexer. No seu caso, isso é apenas:

input = digit | string ;

Quando lexers não podem ser usados ​​facilmente.

Ao projetar um idioma, geralmente cuidamos para que a gramática possa ser separada de maneira limpa em um nível de lexer e um analisador, e que o nível de lexer descreva um idioma regular. Isto nem sempre é possível.

  • Ao incorporar idiomas. Algumas línguas permitem interpolar código em strings: "name={expression}". A sintaxe da expressão faz parte da gramática livre de contexto e, portanto, não pode ser simbolizada por uma expressão regular. Para resolver isso, recombinamos o analisador com o lexer ou introduzimos tokens adicionais como STRING-CONTENT, INTERPOLATE-START, INTERPOLATE-END. A regra de gramática para uma string pode então olhar como: String → STRING-START STRING-CONTENTS { INTERPOLATE-START Expression INTERPOLATE-END STRING-CONTENTS } STRING-END. Obviamente, a Expressão pode conter outras strings, o que nos leva ao próximo problema.

  • Quando os tokens poderiam se conter. Nos idiomas do tipo C, as palavras-chave são indistinguíveis dos identificadores. Isso é resolvido no lexer, priorizando palavras-chave sobre identificadores. Essa estratégia nem sempre é possível. Imagine um arquivo de configuração em que Line → IDENTIFIER " = " REST, onde o resto seja qualquer caractere até o final da linha, mesmo que o restante pareça um identificador. Uma linha de exemplo seria a = b c. O lexer é realmente burro e não sabe em que ordem os tokens podem ocorrer. Portanto, se priorizarmos o IDENTIFIER em vez do REST, o lexer nos fornecerá IDENT(a), " = ", IDENT(b), REST( c). Se priorizarmos REST sobre IDENTIFIER, o lexer nos forneceria REST(a = b c).

    Para resolver isso, precisamos recombinar o lexer com o analisador. A separação pode ser mantida um pouco, tornando o lexer preguiçoso: cada vez que o analisador precisa do próximo token, ele o solicita e diz ao lexer o conjunto de tokens aceitáveis. Efetivamente, estamos criando uma nova regra de nível superior para a gramática lexer de cada posição. Aqui, isso resultaria nas chamadas nextToken(IDENT), nextToken(" = "), nextToken(REST)e tudo funcionaria bem. Isso requer um analisador que conheça o conjunto completo de tokens aceitáveis ​​em cada local, o que implica um analisador de baixo para cima, como LR.

  • Quando o lexer tem que manter o estado. Por exemplo, a linguagem Python delimita os blocos de código não por chaves, mas por indentação. Existem maneiras de lidar com a sintaxe sensível ao layout dentro de uma gramática, mas essas técnicas são um exagero para o Python. Em vez disso, o lexer verifica o recuo de cada linha e emite tokens INDENT se um novo bloco recuado for encontrado, e DEDENT tokens se o bloco tiver terminado. Isso simplifica a gramática principal porque agora pode fingir que esses tokens são como chaves. O lexer, no entanto, agora precisa manter o estado: o recuo atual. Isso significa que o lexer tecnicamente não descreve mais uma linguagem comum, mas na verdade uma linguagem sensível ao contexto. Felizmente, essa diferença não é relevante na prática, e o lexer do Python ainda pode funcionar em O (n) tempo.


Resposta muito boa @amon, obrigado. Vou ter que levar algum tempo para digeri-lo completamente. Eu estava, no entanto, imaginando algumas coisas sobre sua resposta. Por volta do oitavo parágrafo, você mostra como eu poderia modificar minha gramática EBNF de exemplo em regras para um analisador. A gramática que você mostrou também seria usada pelo analisador? Ou ainda existe uma gramática separada para o analisador?
Christian Dean

@ Engenheiro Eu fiz algumas edições. Seu EBNF pode ser usado diretamente por um analisador. No entanto, meu exemplo mostra quais partes da gramática podem ser tratadas por um lexer separado. Quaisquer outras regras ainda serão tratadas pelo analisador principal, mas no seu exemplo isso é justo input = digit | string.
amon

4
A grande vantagem dos analisadores sem scanner é que eles são muito mais fáceis de compor; o exemplo extremo disso são as bibliotecas combinadoras do analisador, nas quais você não faz nada além de compor analisadores. A composição de analisadores é interessante para casos como ECMAScript-incorporado-em-HTML-incorporado-em-PHP-polvilhado-com-SQL-com-uma-linguagem-de-modelo-por-cima ou em Ruby-exemplos-incorporado-em-Markdown- Comentários de documentação incorporados em Ruby ou algo assim.
Jörg W Mittag

O último ponto é muito importante, mas acho que o modo como você o escreveu é enganador. É verdade que os lexers não podem ser usados ​​facilmente com uma sintaxe baseada em recuo, mas a análise sem scanner é ainda mais difícil nesse caso. Então, na verdade, você deseja usar um lexer se tiver esse tipo de idioma, aumentando-o com o estado relevante.
user541686

Os tokens de indentação / dedução acionados por lexer no estilo Python do @Mehrdad só são possíveis para linguagens sensíveis à indentação muito simples e geralmente não são aplicáveis. Uma alternativa mais geral são as gramáticas de atributos, mas seu suporte está ausente nas ferramentas padrão. A idéia é que anotemos todos os fragmentos AST com seu recuo e adicionamos restrições a todas as regras. É fácil adicionar atributos à análise do combinador, o que também facilita a análise sem o scanner.
amon
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.