Vou dar uma facada em colocá-lo nos termos dos leigos.
Se você pensa em termos da árvore de análise (não da AST, mas da visitação e expansão da entrada do analisador), a recursão à esquerda resulta em uma árvore que cresce para a esquerda e para baixo. A recursão correta é exatamente o oposto.
Como exemplo, uma gramática comum em um compilador é uma lista de itens. Vamos pegar uma lista de strings ("vermelho", "verde", "azul") e analisá-la. Eu poderia escrever a gramática de algumas maneiras. Os seguintes exemplos são diretamente à esquerda ou à direita recursivos, respectivamente:
arg_list: arg_list:
STRING STRING
| arg_list ',' STRING | STRING ',' arg_list
As árvores para estas analisam:
(arg_list) (arg_list)
/ \ / \
(arg_list) BLUE RED (arg_list)
/ \ / \
(arg_list) GREEN GREEN (arg_list)
/ /
RED BLUE
Observe como cresce na direção da recursão.
Isso não é realmente um problema, não há problema em escrever uma gramática recursiva esquerda ... se a sua ferramenta de análise puder lidar com isso. Os analisadores de baixo para cima lidam perfeitamente com isso. O mesmo acontece com os analisadores LL mais modernos. O problema com gramáticas recursivas não é recursão, é recursão sem avançar o analisador ou recorrendo sem consumir um token. Se sempre consumimos pelo menos 1 token quando recursamos, chegamos ao final da análise. A recursão esquerda é definida como recorrente sem consumir, que é um loop infinito.
Essa limitação é puramente um detalhe de implementação da implementação de uma gramática com um analisador de LL de cima para baixo ingênuo (analisador de descida recursiva). Se você deseja manter as gramáticas recursivas esquerdas, pode lidar com isso reescrevendo a produção para consumir pelo menos 1 token antes da recorrência, para garantir que nunca fiquemos presos no loop improdutivo. Para qualquer regra gramatical que seja recursiva à esquerda, podemos reescrevê-la adicionando uma regra intermediária que aplique a gramática a apenas um nível de aparência, consumindo um token entre as produções recursivas. (OBSERVAÇÃO: não estou dizendo que essa é a única maneira ou a maneira preferida de reescrever a gramática, apenas apontando a regra generalizada. Neste exemplo simples, a melhor opção é usar a forma recursiva correta). Como essa abordagem é generalizada, um gerador de analisador pode implementá-lo sem envolver o programador (teoricamente). Na prática, acredito que o ANTLR 4 agora faz exatamente isso.
Para a gramática acima, a implementação LL exibindo recursão à esquerda ficaria assim. O analisador começaria com a previsão de uma lista ...
bool match_list()
{
if(lookahead-predicts-something-besides-comma) {
match_STRING();
} else if(lookahead-is-comma) {
match_list(); // left-recursion, infinite loop/stack overflow
match(',');
match_STRING();
} else {
throw new ParseException();
}
}
Na realidade, estamos realmente lidando com "implementação ingênua", ie. inicialmente predicamos uma determinada sentença e, em seguida, recursivamente denominamos a função dessa predição, e essa função ingenuamente chama a mesma predição novamente.
Os analisadores de baixo para cima não têm o problema de regras recursivas em nenhuma direção, porque não reanalisam o início de uma frase, trabalham trabalhando colocando a frase novamente.
A recursão na gramática é apenas um problema se produzirmos de cima para baixo, ou seja. nosso analisador funciona "expandindo" nossas previsões à medida que consumimos tokens. Se, em vez de expandir, colapsarmos (as produções forem "reduzidas"), como em um analisador ascendente LALR (Yacc / Bison), a recursão de ambos os lados não será um problema.
::=
deExpression
paraTerm
e se fizesse o mesmo após a primeira||
, não seria mais recursivo para a esquerda? Mas que se você fizesse apenas depois::=
, mas não||
, ainda seria recursivo à esquerda?