Mi prenderò cura di metterlo nei termini del profano.
Se si pensa in termini dell'albero di analisi (non dell'AST, ma della visita del parser e dell'espansione dell'input), la ricorsione a sinistra provoca un albero che cresce a sinistra e verso il basso. La giusta ricorsione è esattamente l'opposto.
Ad esempio, una grammatica comune in un compilatore è un elenco di elementi. Consente di prendere un elenco di stringhe ("rosso", "verde", "blu") e analizzarlo. Potrei scrivere la grammatica in alcuni modi. I seguenti esempi sono direttamente ricorsivi a sinistra o a destra, rispettivamente:
arg_list: arg_list:
STRING STRING
| arg_list ',' STRING | STRING ',' arg_list
Gli alberi per questi analisi:
(arg_list) (arg_list)
/ \ / \
(arg_list) BLUE RED (arg_list)
/ \ / \
(arg_list) GREEN GREEN (arg_list)
/ /
RED BLUE
Nota come cresce nella direzione della ricorsione.
Questo non è davvero un problema, è giusto voler scrivere una grammatica ricorsiva a sinistra ... se il tuo strumento parser può gestirlo. I parser bottom-up lo gestiscono bene. Così possono i parser LL più moderni. Il problema con le grammatiche ricorsive non è la ricorsione, è la ricorsione senza far avanzare il parser o, ricorrere senza consumare un token. Se consumiamo sempre almeno 1 token quando reclutiamo, alla fine raggiungiamo la fine dell'analisi. La ricorsione sinistra è definita come ricorsione senza consumo, che è un ciclo infinito.
Questa limitazione è puramente un dettaglio di implementazione dell'implementazione di una grammatica con un parser LL top-down ingenuo (parser di discesa ricorsiva). Se si desidera attenersi alle grammatiche ricorsive a sinistra, è possibile gestirle riscrivendo la produzione in modo da consumare almeno 1 token prima di ricorrere, quindi questo assicura che non ci si blocchi mai in un ciclo non produttivo. Per qualsiasi regola grammaticale che è ricorsiva a sinistra, possiamo riscriverla aggiungendo una regola intermedia che appiattisce la grammatica a un solo livello di lookahead, consumando un token tra le produzioni ricorsive. (NOTA: non sto dicendo che questo è l'unico modo o il modo preferito per riscrivere la grammatica, semplicemente sottolineando la regola generalizzata. In questo semplice esempio, l'opzione migliore è usare il modulo ricorsivo a destra). Poiché questo approccio è generalizzato, un generatore di parser può implementarlo senza coinvolgere il programmatore (teoricamente). In pratica, credo che ANTLR 4 ora faccia proprio questo.
Per la grammatica sopra, l'implementazione LL che mostra la ricorsione a sinistra sarebbe simile a questa. Il parser avrebbe iniziato con la previsione di un elenco ...
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();
}
}
In realtà, ciò di cui abbiamo veramente a che fare è la "realizzazione ingenua", vale a dire. inizialmente abbiamo predetto una determinata frase, quindi abbiamo chiamato ricorsivamente la funzione per quella previsione e quella funzione chiama di nuovo ingenuamente la stessa previsione.
I parser bottom-up non hanno il problema delle regole ricorsive in entrambe le direzioni, perché non analizzano l'inizio di una frase, funzionano rimettendo insieme la frase.
La ricorsione in una grammatica è un problema solo se produciamo dall'alto verso il basso, cioè. il nostro parser funziona "espandendo" le nostre previsioni mentre consumiamo i token. Se invece di espanderci, collassiamo (le produzioni sono "ridotte"), come in un parser bottom-up LALR (Yacc / Bison), allora la ricorsione di entrambe le parti non è un problema.
::=
daExpression
aTerm
, e se facessi lo stesso dopo la prima||
, non sarebbe più ricorsivo? Ma che se lo facessi solo dopo::=
, ma non||
sarebbe ancora ricorsivo?