Quando o lexing é suficiente, quando você precisa do EBNF?
O EBNF realmente não acrescenta muito ao poder das gramáticas. É apenas uma notação de conveniência / atalho / "açúcar sintático" sobre as regras gramaticais padrão da Chomsky's Normal Form (CNF). Por exemplo, a alternativa EBNF:
S --> A | B
você pode obter no CNF listando cada produção alternativa separadamente:
S --> A // `S` can be `A`,
S --> B // or it can be `B`.
O elemento opcional do EBNF:
S --> X?
você pode obter no CNF usando uma produção anulável , ou seja, aquela que pode ser substituída por uma cadeia vazia (denotada apenas pela produção vazia aqui; outros usam epsilon, lambda ou círculo cruzado):
S --> B // `S` can be `B`,
B --> X // and `B` can be just `X`,
B --> // or it can be empty.
Uma produção em uma forma como a última B
acima é chamada de "apagamento", porque pode apagar o que quer que seja em outras produções (produto uma cadeia vazia em vez de outra coisa).
Zero ou mais repetições de EBNF:
S --> A*
você pode obter usando a produção recursiva , ou seja, uma que se incorpora em algum lugar nela. Isso pode ser feito de duas maneiras. O primeiro é a recursão à esquerda (que geralmente deve ser evitada, porque os analisadores de descida recursiva de cima para baixo não podem analisá-la):
S --> S A // `S` is just itself ended with `A` (which can be done many times),
S --> // or it can begin with empty-string, which stops the recursion.
Sabendo que gera apenas uma string vazia (no final das contas) seguida por zero ou mais A
s, a mesma string ( mas não o mesmo idioma! ) Pode ser expressa usando a recursão correta :
S --> A S // `S` can be `A` followed by itself (which can be done many times),
S --> // or it can be just empty-string end, which stops the recursion.
E quando se trata +
de uma ou mais repetições do EBNF:
S --> A+
isso pode ser feito fatorando um A
e usando *
como antes:
S --> A A*
que você pode expressar no CNF como tal (eu uso a recursão correta aqui; tente descobrir o outro como exercício):
S --> A S // `S` can be one `A` followed by `S` (which stands for more `A`s),
S --> A // or it could be just one single `A`.
Sabendo disso, agora você provavelmente pode reconhecer uma gramática para uma expressão regular (ou seja, gramática regular ) como aquela que pode ser expressa em uma única produção EBNF que consiste apenas em símbolos terminais. De maneira mais geral, você pode reconhecer gramáticas regulares quando vê produções semelhantes a estas:
A --> // Empty (nullable) production (AKA erasure).
B --> x // Single terminal symbol.
C --> y D // Simple state change from `C` to `D` when seeing input `y`.
E --> F z // Simple state change from `E` to `F` when seeing input `z`.
G --> G u // Left recursion.
H --> v H // Right recursion.
Ou seja, usando apenas strings vazias, símbolos terminais, simples não terminais para substituições e alterações de estado, e usando recursão apenas para obter repetição (iteração, que é apenas recursão linear - aquela que não se ramifica como árvore). Nada mais avançado acima disso, então você tem certeza de que é uma sintaxe regular e pode usar apenas um pouco mais para isso.
Mas quando sua sintaxe usa a recursão de maneira não trivial, para produzir estruturas aninhadas semelhantes a árvores, auto-similares, como a seguinte:
S --> a S b // `S` can be itself "parenthesized" by `a` and `b` on both sides.
S --> // or it could be (ultimately) empty, which ends recursion.
então você pode ver facilmente que isso não pode ser feito com expressão regular, porque você não pode resolvê-lo em uma única produção de EBNF de forma alguma; você vai acabar com substituindo S
indefinidamente, o que será sempre adicionar mais a
s e b
s em ambos os lados. Os Lexers (mais especificamente: Autômatos de Estado Finito usados por lexers) não podem contar com um número arbitrário (eles são finitos, lembra-se?); Portanto, eles não sabem quantos a
s havia para combiná-los igualmente com tantos b
s. Gramáticas como essa são chamadas de gramáticas sem contexto (no mínimo) e requerem um analisador.
As gramáticas livres de contexto são conhecidas por analisar, portanto são amplamente usadas para descrever a sintaxe das linguagens de programação. Mas tem mais. Às vezes, é necessária uma gramática mais geral - quando você tem mais coisas para contar ao mesmo tempo, independentemente. Por exemplo, quando você deseja descrever um idioma em que é possível usar parênteses redondos e chavetas entrelaçadas, mas elas precisam ser emparelhadas corretamente umas com as outras (chavetas com chavetas, arredondadas com arredondadas). Esse tipo de gramática é chamado sensível ao contexto . Você pode reconhecê-lo por ter mais de um símbolo à esquerda (antes da seta). Por exemplo:
A R B --> A S B
Você pode pensar nesses símbolos adicionais à esquerda como um "contexto" para aplicar a regra. Poderia haver algumas pré-condições, pós-condições, etc. Por exemplo, a regra acima irá substituir R
em S
, mas apenas quando está no meio A
e B
, deixando os A
e B
-se inalterado. Esse tipo de sintaxe é realmente difícil de analisar, porque precisa de uma máquina de Turing completa. É uma história totalmente diferente, então vou terminar aqui.