Motivo desiderato per l'espressione che analizza in RPN

1

Sto scrivendo un interprete / compilatore

Ho riscontrato un problema che ho risolto in precedenza, ma forse non in modo ottimale.

Il problema va così, le espressioni possono esprimere molte cose come ambiti, assegnazioni di variabili e dichiarazioni.

Quando un'espressione è semplice, ad esempio:

a = 4 + 3

trasformarlo in una notazione polacca inversa è abbastanza semplice, usando l'algoritmo di manovrabilità che posso trasformare in:

4 3 + a =

eseguirlo è abbastanza facile, ma con la crescente complessità delle espressioni sembra impossibile.

ad esempio:

Array<Class<T,U>> b = (x,y)=>{int x = k; return func1(k*y,2);}

Trasformerò questa espressione complessa in una notazione a smalto inverso di soli token?

La sintassi non è troppo ambigua per questo?

Mi sembra che mentre potrei implementarlo con una serie di token grezzi, è probabilmente improbabile che questo sia il modo in cui i compilatori di solito lo affrontano perché sembra irrinunciabile.

D'altra parte potrei scrivere subparser per l'espressione rendendolo un'espressione di sottoespressioni come:

{typeexpression} {identifiertoken} {operatortoken} {lambda expression}..

Quindi la domanda si arresta, qual è il modo più corretto per scrivere un parser di espressioni estensibile ma ancora fedele alla progettazione del compilatore.

Come nodo laterale: non ho studiato la progettazione del compilatore, quindi qualsiasi risorsa o suggerimento mi avrebbe aiutato molto, il codice corrente per l'interpeter (che è scritto in c #) si trova su GitHub in caso si voglia vedere qualche codice I ho scritto male commentare il repositorty

    
posta downrep_nation 28.08.2017 - 12:48
fonte

1 risposta

2

L'algoritmo dello shunting yard è per un problema di parsing molto specifico: analizzare le espressioni con la precedenza degli operatori. Può essere utilizzato quando tutte le produzioni nella grammatica hanno la forma ricorsiva Expr → Expr op Expr e le produzioni sono classificate in base alla priorità. (Più ovviamente casi di base della forma Expr → terminal e produzioni per operatori unari)

La maggior parte dei linguaggi di programmazione non si adatta a questa semplice struttura. Anche l'assegnazione = non è un operatore ordinario perché la parte sinistra potrebbe non essere un'espressione arbitraria: 42 * 3 = a + 7 è non valido nella maggior parte dei linguaggi di programmazione. In quanto tale, l'operatore di assegnazione non può essere analizzato dall'algoritmo dello shunting yard.

Questa restrizione non ha nulla a che fare con RPN contro alberi di sintassi astratti. L'RPN è esattamente equivalente a un albero, l'albero è solo appiattito.

Sebbene l'algoritmo dello shunting yard non sia abbastanza potente per analizzare i linguaggi di programmazione "reali", è un buon trampolino di lancio per comprendere algoritmi di analisi bottom-up più complessi come LR. Quindi, poiché dovremo utilizzare comunque un algoritmo più potente, l'algoritmo dello shunting yard non viene utilizzato nella pratica.

Molte implementazioni del linguaggio di programmazione reale utilizzano un generatore di parser (talvolta chiamato anche compiler-compilatore). Con un generatore di parser annoti le regole della tua grammatica in una sorta di notazione EBNF e lo strumento genera un programma che analizzerà questa grammatica. Questo è abbastanza gestibile e può utilizzare buoni algoritmi di parsing.

In alternativa, puoi scrivere un compilatore a mano. Questo è in realtà abbastanza comune perché questo può potenzialmente produrre messaggi di errore molto migliori o gestire le ambiguità che un generatore di parser non può indirizzare. È anche molto più difficile avere ragione.

La maggior parte dei parser scritti a mano usano una strategia di discesa ricorsiva , che è fondamentalmente la tua idea di sub-parser: implementiamo ogni regola nella nostra grammatica come una funzione. La funzione accetta lo stato del parser come input e restituisce un frammento di albero della sintassi. Ad esempio una regola Assignment → Variable "=" Expr ";" potrebbe essere implementata in questo modo:

class Parser {
  ... // keep parser state as instance fields

  Assignment parseAssignment() {
    Variable variable = parseVariable();
    consumeLiteral("=");
    Expression expr = parseExpr();
    consumeLiteral(";");
    return new Assignment(variable, expr);
  }
}

Il problema con i parser ricorsivi-discendenti è che hanno difficoltà a gestire produzioni left-ricorsive (cioè il simbolo della mano sinistra di una produzione è il primo simbolo sul lato destro, come X → X ... ). Ma queste regole sono comuni nelle espressioni matematiche!

Una possibilità legittima è quella di utilizzare l'algoritmo dello shunting-yard per questo, perché si occupa in modo specifico di questi casi. In alternativa, questo può anche essere risolto eliminando esplicitamente la ricorsione. Per esempio. le regole Term -> Term "*" Factor; Term -> Factor potrebbero essere ingenuamente implementate come

Expression parseTerm() {
  Expression left = parseTerm();  // FIXME infinite recursion
  if (!peekLiteral("*")) return left;
  consumeLiteral("*");
  Expression right = parseFactor();
  return new Multiplication(left, right);
}

Al contrario, la regola può essere implementata correttamente senza ricorsione ma con un ciclo:

Expression parseTerm() {
  Expression expr = parseFactor();
  while (peekLiteral("*")) {
    consumeLiteral("*");
    Expression right = parseFactor();
    expr = new Multiplication(expr, right);
  }
  return expr;
}
    
risposta data 30.08.2017 - 23:37
fonte

Leggi altre domande sui tag