In parole povere, cos'è la ricorsione sinistra?


12

Secondo una pagina su code.google.com, la "ricorsione a sinistra" è definita come segue:

La ricorsione a sinistra si riferisce a qualsiasi non ricorrente ricorsivo che, quando produce una forma sentenziale contenente se stesso, quella nuova copia di se stessa appare a sinistra della regola di produzione.

Wikipedia offre due diverse definizioni:

  1. In termini di grammatica senza contesto, una r non terminale è ricorsiva a sinistra se il simbolo più a sinistra in una delle produzioni di r ("alternative") o immediatamente (ricorsivo diretto / immediato a sinistra) o attraverso qualche altra non terminale le definizioni (indirette / nascoste ricorsive a sinistra) riscrivono nuovamente in r.

  2. "Una grammatica è ricorsiva a sinistra se riusciamo a trovare una A non terminale che alla fine trarrà una forma sentenziale con se stessa come simbolo di sinistra."

Sto appena iniziando con la creazione della lingua qui, e lo sto facendo nel mio tempo libero. Tuttavia, quando si tratta di selezionare un parser di lingua, se la ricorsione sinistra è supportata da questo parser o quel parser è un problema che si pone immediatamente in primo piano. Cercare termini come "forma sentenziale" porta solo a ulteriori elenchi di gergo, ma la distinzione della ricorsione "sinistra" deve quasi essere qualcosa di molto semplice. Traduzione perfavore?

Risposte:


21

Una regola Rè ricorsiva a sinistra se, per scoprire se le Rpartite, devi prima scoprire se le Rpartite. Ciò accade quando Rappare, direttamente o indirettamente, come il primo termine in qualche produzione di se stesso.

Immagina una versione giocattolo della grammatica per le espressioni matematiche, con solo l'aggiunta e la moltiplicazione per evitare distrazioni:

Expression ::= Multiplication '+' Expression
            || Multiplication

Multiplication ::= Term '*' Term
                 || Term

Term ::= Number | Variable

Come scritto, non c'è ricorsione a sinistra qui - potremmo passare questa grammatica a un parser a discesa ricorsiva.

Supponiamo che tu abbia provato a scriverlo in questo modo:

Expression ::= Expression '*' Expression
            || Expression '+' Expression
            || Term

Term ::= Number | Variable

Questa è una grammatica e alcuni parser possono farcela, ma i parser di discesa ricorsivi e i parser LL non possono, perché la regola per Expressioninizia con Expressionse stessa. Dovrebbe essere ovvio il motivo per cui in un parser a discesa ricorsiva questo porta a una ricorsione illimitata senza effettivamente consumare alcun input.

Non importa se la regola si riferisce a se stessa direttamente o indirettamente; se Aha un'alternativa che inizia con B, e Bha un'alternativa che inizia con A, allora Ae Bsono entrambi indirettamente ricorsivi a sinistra, e in un parser ricorsivo-discendente le loro funzioni di abbinamento porterebbero a una ricorsione reciproca senza fine.


Quindi nel secondo esempio, se cambiassi la prima cosa dopo ::=da Expressiona Term, e se facessi lo stesso dopo la prima ||, non sarebbe più ricorsivo? Ma che se lo facessi solo dopo ::=, ma non ||sarebbe ancora ricorsivo?
Panzercrisis,

Sembra che tu stia dicendo che molti parser vanno da sinistra a destra, fermandosi ad ogni simbolo e valutandolo ricorsivamente sul posto. In questo caso, se Expressionsi dovesse spegnere il primo , Termsia dopo ::=che dopo il primo ||, tutto andrebbe bene; perché prima o poi, sarebbe eseguito in qualcosa che non è né un NumberVariable, essendo quindi in grado di determinare che qualcosa non è un Expressioncolpo ulteriore esecuzione ...
Panzercrisis

... Ma se uno di questi fosse ancora iniziato Expression, avrebbe potenzialmente trovato qualcosa che non era un Term, e avrebbe continuato a controllare se tutto fosse Expressionripetutamente. È così?
Panzercrisis,

1
@Panzercrisis più o meno. Devi davvero cercare i significati dei parser LL, LR e di discesa ricorsiva.
Hobbs,

Questo è tecnicamente accurato, ma forse non abbastanza semplice (i termini del laico). Aggiungo anche che, in pratica, i parser LL avranno in genere la capacità di rilevare la ricorsione e di evitarla (potenzialmente rifiutando stringhe inventate valide nel processo), così come il fatto che in pratica la maggior parte dei linguaggi di programmazione ha una grammatica definita in in modo tale da evitare la ricorsione infinita.

4

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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.