[ Inhalt ] [ Index ]

Next: Bottom-up-Analyse Up: Syntaktische Analyse Previous: BNF und EBNF

Top-down-Analyse

Bei der Topdown-Analyse   wird ausgehend von der Wurzel des Ableitungsbaums eine Ableitungsfolge   gesucht. Wenn man bei diesem Verfahren ohne Backtracking   auskommen will, ist intuitiv klar, daß man in jedem Ableitungsschritt eindeutig entscheiden können muß, welche Produktion als nächste angewendet werden soll. Um diese Auswahl zu erleichtern, betrachtet man nicht nur das aktuelle Eingabesymbol, sondern man schaut (maximal k Symbole) voraus.

LL(k)-Grammatiken

Definition:
Eine kontexfreie Grammatik G = (V,T,P,S) heißt LL(k)-Grammatik  , wenn ein Ableitungsschritt eindeutig durch k Symbole der Eingabe (lookahead  ) bestimmt ist.

Die folgende Grammatik ist LL(1):

tex2html_wrap_inline2295 :
tex2html_wrap_inline2297
tex2html_wrap_inline2299

Beginnt ein Wort mit a wird die erste, sonst die zweite Alternative für S gewählt. Soll B abgeleitet werden, kann man auch immer am ersten Zeichen entscheiden, welche Produktion angewendet werden muß.

Das funktioniert bei der folgenden Grammatik nicht:

tex2html_wrap_inline2307 :
tex2html_wrap_inline2309
tex2html_wrap_inline2311
tex2html_wrap_inline2313

Wenn man einen Satz ableiten will, der mit ccc beginnt, kann man mit dem ersten c nicht entscheiden, ob tex2html_wrap_inline2319 oder tex2html_wrap_inline2321 angewendet werden soll.

Es gelten die folgenden Sätze:

Satz:
LL(k)-Grammatiken sind eindeutig.

Satz:
Es gibt kontextfreie Grammatiken, die für kein k LL(k) sind.

Satz:
Es gibt kontextfreie Grammatiken, die zwar LL(k) aber nicht LL(k-1) sind.

Satz:
Es ist entscheidbar, ob eine Grammatik für ein festes k LL(k) ist.

Satz:
Es ist nicht entscheidbar, ob für eine Grammatik ein k existiert, so daß die Grammatik LL(k) ist.

Rekursiver Abstieg

Wenn wir den regulären Ausdruck: (a+b)(a+b+c+d)* betrachten, kann man sehr einfach eine Funktion angeben, die ein Wort dieser Sprache akzeptiert:

int Parse()
{
 if ((sy == `a`) || (sy == 'b')) Nextsy();
 else return ERROR;
 while ((sy >= 'a') && (sy <= 'd')) Nextsy();
 if (sy != EOF)
  return ERROR;
 return ACCEPT;
}

Eine kontextfreie Sprache, deren Grammatik die LL(1)-Eigenschaft hat, kann man nun ganz ähnlich analysieren. Dazu ein

Beispiel:

tex2html_wrap_inline2295 :
tex2html_wrap_inline2329
tex2html_wrap_inline2331
tex2html_wrap_inline2333

int S()
{
 A();
 if (sy == EOF) return ACCEPT;
 else return ERROR;
}

void A()
{
 if (sy == 'a')
 {
  Nextsy(); B(); A(); 
 }
 else if (sy == 'b') Nextsy();
      else Error();
}

void B()
{
 if (sy == 'a') Nextsy();
 else if (sy == 'b')
      {
       Nextsy(); A(); B();
      }
      else Error();
}

Wir haben in diesem Beispiel für jede Variable der Grammatik, die in EBNF-Format   vorliegen muß, eine Funktion geschrieben, die die Satzformen, die aus den zugehörigen rechten Seiten abgeleitet werden können, analysiert. Wenn terminale Symbole auftreten, werden diese mit den aktuellen Eingabesymbolen verglichen und überlesen. Variablen auf der rechten Seite der Produktion führen zu einem (möglicherweise rekursiven) Aufruf einer Analysefunktion.

Rekursiver Abstieg für arithmetische Ausdrücke

Produktionen von OBERON-0 für arithmetische Ausdrücke

 

Factor           = ident Selector | integer | 
                   ``(`` Expression ``)'' | ``~`` Factor.
Term             = Factor {(``*'' | ``DIV'' | 
                            ``MOD'' | ``&'') Factor}.
SimpleExpression = [``+'' | ``-''] Term 
                   {(``+'' | ``-'' | ``OR'') Term}.
Expression       = SimpleExpression
                   [(``='' | ``#'' | ``<'' | 
                     ``<='' | ``>'' | ``>='') 
                    SimpleExpression].

Analysefunktionen

void Expression(){
 int lsy;

 SimpleExpression();
 if ((nextsymbol >= eqsy) && (nextsymbol <= gesy))
 {
  lsy = nextsymbol;
  Insymbol();
  SimpleExpression();
 }
}

void SimpleExpression(){
 int lop;
 int sign = addop;

 if ((nextsymbol == addop) || (nextsymbol == subop)) 
 {sign = nextsymbol; Insymbol();}
 Term();
 while ((nextsymbol >= addop) && (nextsymbol <= subop))
 {
  lop = nextsymbol;
  Insymbol();
  Term();
 }
}

void Term(){
 int lop;
 
 Factor();
 while ((nextsymbol >= multop) || (nextsymbol <= andop))
 {
  lop = nextsymbol;
  Insymbol();
  Factor();
 }
}

void Factor(){
 if (nextsymbol == lpar)
 {Insymbol();
  Expression();
  if (nextsymbol == rpar)
  {Insymbol();}
  else Error(") expected");
 }
 else if (nextsymbol == integer)
 {Insymbol();}
 else if (nextsymbol == ident)
      {Insymbol(); Selector()}
      else if (nextsymbol == notsy)
           {Insymbol(); Factor();
           }
           else Error("Error in Factor\n"); 
}

int main(){
 Insymbol();
 while (nextsymbol)
 {
  Expression();
 };
 return 0;
}

 

Linksrekursionen

Eine Grammatik, die Produktionen der Form tex2html_wrap_inline2337 enthält, führt beim rekursiven Abstieg zu Problemen.

Beispiel:

tex2html_wrap_inline2339
tex2html_wrap_inline2341
tex2html_wrap_inline2343

Die naive Umsetzung führt zu folgendem Code:

void E()
{
 E();
 Insymbol(); // + Überlesen
 T();
}

Da dauert es nicht lange, bis der Laufzeitkeller überläuft!

Diese Linksrekursion kann man aber leicht beseitigen:

tex2html_wrap_inline2345

Das geht auch allgemein:  

tex2html_wrap_inline2347

wird ersetzt durch:

tex2html_wrap_inline2349
tex2html_wrap_inline2351

Im Beispiel ist tex2html_wrap_inline2353 und A = E. Damit ergibt sich:

tex2html_wrap_inline2357 und tex2html_wrap_inline2359

Die Linksrekursion ist beseitigt, falls sich tex2html_wrap_inline2046 nicht zu tex2html_wrap_inline1843 ableiten läßt. Die Sprache bleibt unverändert.

Linksfaktorisierung

 

Bei einer Produktion der Form

tex2html_wrap_inline2366

ist die Auswahl der richtigen Produktion erst nach Abarbeitung von tex2html_wrap_inline2046 möglich. Auch dieses Problem kann man lösen:

tex2html_wrap_inline2370
tex2html_wrap_inline2372

FIRST und FOLLOW

In diesem Abschnitt geht es um die Frage: wie entscheidet man, ob eine Grammatik LL(1) ist oder nicht.

Damit eine Grammatik diese Eigenschaft hat, muß ja sichergestellt sein, daß man mit einem Symbol Lookahead entscheiden kann, welche von zwei alternativen Produktionen gewählt werden soll. Falls die beiden Alternativen tex2html_wrap_inline2375 lauten, dann dürfen doch zunächst einmal die Zeichenketten, die aus tex2html_wrap_inline2046 bzw. tex2html_wrap_inline2379 abgeleitet werden können, nicht mit den gleichen terminalen Symbolen anfangen.

Die Funktion FIRST( tex2html_wrap_inline2046 ) berechnet nun gerade die Terminals, mit denen aus tex2html_wrap_inline2046 abgeleitete Satzformen beginnen.

Falls man Produktionen der Art tex2html_wrap_inline2375 hat, kann ein weiteres Problem auftreten. Wenn man aus tex2html_wrap_inline2379 das leere Wort ableiten kann, hängt die Entscheidung, welche Produktion gewählt werden soll, davon ab, ob FIRST( tex2html_wrap_inline2046 ) und die Menge der terminalen Symbole, die auf A folgen können, disjunkt sind. In diesem Fall kann man eindeutig entscheiden, ob tex2html_wrap_inline2379 zu tex2html_wrap_inline1843 abgeleitet, oder aber tex2html_wrap_inline2046 gewählt wurde. Die Symbole, die in Satzformen auf A folgen können, werden mit FOLLOW(A) berechnet.

FIRST und FOLLOW: Formale Definition

   

Um festzustellen, ob eine kontextfreie Grammatik G = (V,T,P,S) die LL(1)-Eigenschaft   besitzt, definieren wir zwei Mengen:

Definition:
Sei tex2html_wrap_inline2044 . Dann ist
FIRST( tex2html_wrap_inline2046 ) tex2html_wrap_inline2409 tex2html_wrap_inline2056 tex2html_wrap_inline2413

FIRST enthält also die terminalen Symbole, mit denen Ableitungen aus tex2html_wrap_inline2046 beginnen können.

Definition:
Sei tex2html_wrap_inline2417 . Dann ist
FOLLOW(A) tex2html_wrap_inline2421 tex2html_wrap_inline2056 tex2html_wrap_inline2425

FOLLOW beinhaltet also die terminalen Symbole, die in Satzformen
direkt auf A folgen können.

Satz:
Eine Grammatik ist genau dann LL(1), wenn für je zwei Produktionen tex2html_wrap_inline2375 gilt:
1. FIRST( tex2html_wrap_inline2046 ) tex2html_wrap_inline2431 FIRST( tex2html_wrap_inline2379 ) = tex2html_wrap_inline1988
2. tex2html_wrap_inline2437 FIRST( tex2html_wrap_inline2379 ) tex2html_wrap_inline2441 FIRST( tex2html_wrap_inline2046 ) tex2html_wrap_inline2431 FOLLOW(A) tex2html_wrap_inline2449

Konstruktion von FIRST

  1. tex2html_wrap_inline2454 FIRST(X) tex2html_wrap_inline2458 , d.h.
    ist X ein Terminal, dann ist X in FIRST(X)
  2. tex2html_wrap_inline2466 und tex2html_wrap_inline2468
    FIRST(X) = FIRST(X) tex2html_wrap_inline2474 , d.h.
    wenn die rechte Seite einer Regel für X mit einem Terminal a beginnt, so muß a in FIRST(X) enthalten sein.
  3. tex2html_wrap_inline2466 und tex2html_wrap_inline2486 FIRST(X) = FIRST(X) tex2html_wrap_inline2492 , d.h
    wenn das leere Wort aus X abgeleitet werden kann, ist tex2html_wrap_inline1843 in FIRST(X).
  4. tex2html_wrap_inline2500 tex2html_wrap_inline2056 tex2html_wrap_inline1843

    FIRST(X) = FIRST(X) tex2html_wrap_inline2510 FIRST( tex2html_wrap_inline2512 ) tex2html_wrap_inline2514

    Wenn tex2html_wrap_inline2516 FIRST(X) = FIRST(X) tex2html_wrap_inline2492
    Dies ist der etwas schwierigere Fall. Die Regel bedeutet einfach, wenn eine Produktion tex2html_wrap_inline2524 existiert, und aus tex2html_wrap_inline2526 ist tex2html_wrap_inline1843 ableitbar, so muß man FIRST( tex2html_wrap_inline2530 ) zu FIRST(X) hinzufügen. Falls nun aber auch tex2html_wrap_inline2530 auf tex2html_wrap_inline1843 führt, so muß FIRST( tex2html_wrap_inline2538 ) zu FIRST(X) hinzugefügt werden (jeweils ohne tex2html_wrap_inline1843 ). Nur wenn alle tex2html_wrap_inline2512 auf tex2html_wrap_inline1843 führen, muß tex2html_wrap_inline1843 zu FIRST(X) getan werden.

FIRST( tex2html_wrap_inline2552 ) berechnet man nun so:

Alle nicht- tex2html_wrap_inline1843 -Symbole aus FIRST( tex2html_wrap_inline2556 ) gehören zu FIRST( tex2html_wrap_inline2552 ).

Ist tex2html_wrap_inline2437 FIRST( tex2html_wrap_inline2556 ), dann gehören auch die nicht- tex2html_wrap_inline1843 -Symbole aus FIRST( tex2html_wrap_inline2566 ) zu FIRST( tex2html_wrap_inline2552 ).

Ist tex2html_wrap_inline2437 FIRST( tex2html_wrap_inline2566 ), dann gehören auch die nicht- tex2html_wrap_inline1843 -Symbole aus FIRST( tex2html_wrap_inline2576 ) zu FIRST( tex2html_wrap_inline2552 )

u.s.w.

Ist tex2html_wrap_inline2437 FIRST( tex2html_wrap_inline2582 ) tex2html_wrap_inline2584 , dann gehört auch tex2html_wrap_inline1843 zu FIRST( tex2html_wrap_inline2552 ).

Konstruktion von FOLLOW

Um für tex2html_wrap_inline2417 FOLLOW(A) zu berechnen, wende man die folgenden drei Regeln an, bis sich keine Änderung mehr ergibt:

  1. $, ein spezielles Endsymbol ist in FOLLOW(S)
  2. tex2html_wrap_inline2596

    FOLLOW(B) enthält außer tex2html_wrap_inline1843 alles aus FIRST( tex2html_wrap_inline2379 ).

  3. tex2html_wrap_inline2604 oder

    tex2html_wrap_inline2606 und FIRST( tex2html_wrap_inline2379 ) enthält tex2html_wrap_inline1843 ,

    dann ist alles aus FOLLOW(A) in FOLLOW(B).

 

Parsingtabellen

Mit FIRST und FOLLOW läßt sich auch ein tabellengesteuerter Parser   für eine Grammatik G = (V,T,P,S) konstruieren. Das wird man zwar in der Regel nicht manuell machen - aber man kann an diesem Algorithmus recht gut sehen, wie ein Parsergenerator   prinzipiell funktioniert. Beim rekursiven Abstieg wird letztlich genau das gleiche Verfahren angewendet - nur nimmt einem das Laufzeitsystem die explizite Verwaltung des Parserstacks ab.

Zunächst wird eine Matrix M konstruiert, die den Parser steuert. M hat die folgende Form:

  1. für jedes tex2html_wrap_inline2622 existiert eine Zeile
  2. für jedes tex2html_wrap_inline2624 existiert eine Spalte
  3. M[v,t] wird mit tex2html_wrap_inline1990 initialisiert
  4. für alle Regeln tex2html_wrap_inline2630
    1. tex2html_wrap_inline2632
    2. wenn tex2html_wrap_inline2634
      für alle tex2html_wrap_inline2636
  5. alle undefinierten Einträge werden auf error gesetzt.

Wenn es jetzt keine Mehrfacheinträge gibt, kann man für jede Kombination von Symbolen aus tex2html_wrap_inline2638 entscheiden, welche Produktion angewendet werden soll, und es gilt der

Satz:

Eine Grammatik ist LL(1), wenn die zugehörige Parsingtabelle keine Mehrfacheinträge enthält.

Beispiel:

Wir betrachten wieder die übliche Grammatik für arithmetische Ausdrücke:

tex2html_wrap_inline2640
tex2html_wrap_inline2642
tex2html_wrap_inline2644

tex2html_wrap_inline2646

FOLLOW brauchen wir nicht zu berechnen, da keine tex2html_wrap_inline1843 -Produktionen existieren.

Dann hat M die Form:

M id + * ( ) $
E tex2html_wrap_inline2658 tex2html_wrap_inline2658
T tex2html_wrap_inline2664 tex2html_wrap_inline2664
F id (E)


Offenbar ist diese Grammatik nicht LL(1).

Mit dem Verfahren von Seite gif können wir die Grammatik aber leicht umformen und erhalten:

tex2html_wrap_inline2674
tex2html_wrap_inline2676
tex2html_wrap_inline2678
tex2html_wrap_inline2680
tex2html_wrap_inline2644

Wir berechnen FIRST und FOLLOW:

FIRST FOLLOW
E id, ( $, )
E' +, tex2html_wrap_inline1843 $, )
T id, ( +, $, )
T' *, tex2html_wrap_inline1843 +, $, )
F id, ( *, +, $, )


Somit erhalten wir für die Parsingtabelle M:

M id + * ( ) $
E TE' TE'
E' +TE' tex2html_wrap_inline1843 tex2html_wrap_inline1843
T FT' FT'
T' tex2html_wrap_inline1843 *FT' tex2html_wrap_inline1843 tex2html_wrap_inline1843
F id (E)

Der Parsingalgorithmus:

G = (V,T,P,S) und tex2html_wrap_inline2748 Parsingtabelle

tex2html_wrap_inline2750 , terminiert mit $

TopOfStack = S

actsy = 0; 
while (actsy < strlen(s))
{
 if (``TOS aus V'')
 {
  if (M[TOS, s[actsy]] != ERROR)
  {``POP(); PUSH(M[TOS,s[actsy]]);}
  else EXIT;
 }
 else // TOS aus T
 {
  if (TOS == s[actsy++]) POP();
  else EXIT;
 }
 if (``stack leer'' && (actsy == strlen(s)))
  ACCEPT;
 else EXIT;
}

 

Fehlerbehandlung

So wie wir die Syntaxanalyse bisher betrachtet haben, bricht der Parser seine Arbeit beim ersten Syntaxfehler ab. Das ist natürlich in der Praxis kein angemessenes Vorgehen. Vielmehr sollen ja möglichst viele Fehler aufgedeckt und sinnvolle Fehlermeldungen generiert werden.

Dies ist aber häufig gar nicht so einfach, denn um nach einem Fehler weitermachen zu können, müssen gewisse Annahmen über den Programmtext getroffen werden, damit Teile eingefügt oder überlesen werden können.

Für den Compiler besteht zwischen einem fehlenden Semikolon und einem fehlenden arithmetischen Operator kein prinzipieller Unterschied. Ein menschlicher Programmierer wird aber wesentlich seltener ein Pluszeichen als ein Semikolon vergessen.

Ein erfahrener Programmierer wird auch ganz andere Fehler machen als ein Anfänger bei seinen ersten C++-Gehversuchen. Daher ist es kaum möglich, allgemeingültige Regeln für die Fehlerbehandlung aufzustellen. Wir werden aber einige nützliche Heuristiken kennenlernen.

 

Qualitätskriterien für die Fehlerbehandlung (Wirth )

  1. Möglichst viele Fehler in einem Durchlauf erkennen
  2. Analyse fehlerfreier Texte nicht bremsen
  3. Parser nicht unverhältnismäßig aufblähen

Offenbar spielt die konkrete Syntax     hier eine große Rolle. Je einfacher die Sprachstruktur, desto besser die Möglichkeiten der Fehlerbehandlung.

Man kann prinzipiell zwei Fehlerkategorien   unterscheiden:

1. Fehlende Symbole: a := b * (x + y;
Stellt der Parser die fehlende Klammer fest, wird eine Meldung erzeugt und weiter analysiert. Unangenehm ist aber der Fall, daß das Fehlen von Symbolen zu spät bemerkt wird. Dann kann man oft nicht mehr entscheiden, ob ein Symbol fehlt, oder ob ein Symbol zuviel vorhanden ist:
a := z * b * (x + y));

2. Falsche Symbole: a = b statt a := b
Bei Verwendung eines falschen Symbols wird das falsche Symbol überlesen und weitergemacht. Eventuell muß man aber Folgesymbole bis zu einer Synchronisationsstelle   überlesen.

Im zweiten Fall kann die konkrete Syntax der Sprache helfen. So fangen in OBERON Deklarationen stets mit CONST, TYPE, VAR oder
PROCEDURE an, Anweisungen mit IF, WHILE oder einem Identifikator. Bei Syntaxfehlern kann man daher bis zu diesen Symbolen ''vorspulen'' und die Analyse an der Synchronisationsstelle fortsetzen.

Wir geben daher jeder Analysefunktion zwei Parameter mit: der erste beschreibt die Menge der gültigen Anfangssymbole eines Konstrukts. So erhält die Funktion Statement die Symbole ident, whilesy, ifsy. Diese Symbole entsprechen FIRST(Statement).  

Der zweite Parameter beschreibt die Symbole, die zur Synchronisation dienen sollen und deshalb keinesfalls überlesen werden dürfen. Dies sind die Symbole, die in Satzformen unmittelbar hinter dem aktuellen Nonterminal auftreten können: die FOLLOW-Menge.  

In einer Programmiersprache wie Pascal   lassen sich diese Mengen direkt abbilden. Wir betrachten einen Ausschnitt aus einem PASCAL-Compiler

typebeginsys := [add,sub,intconst,realconst,
                 charconst,ident,stringsy, 
                 lparent,arrow,packedsy,
                 arraysy,recordsy,setsy,filesy];
statbeginsys := [beginsy,gotosy,ifsy,whilesy,
                 repeatsy,loopsy,
                 forsy,withsy,casesy];
blockbeginsys:= [labelsy,constsy,typesy,varsy,
                 valuesy,initprocsy,
                 funcsy,procsy];
factorbegsys := [intconst,realconst,stringconst,
                 charconst,ident,
                 lparent,lbrack,notsy,nilsy];

In OBERON-0 erhalten wir

typebeginsys := [ident, arraysy, recordsy];
statbeginsys := [beginsy, ifsy, whilesy, ident];
blockbeginsys:= [constsy, typesy, varsy, proceduresy];
factorbegsys := [integer, ident, lparent, notsy];

In C++   muß man etwas mehr Arbeit aufwenden, um die Mengen zu emulieren. Eine Möglichkeit ist eine geschickte Anordnung der Symbole, so daß man Bereiche testen kann:

if ((sy >= ifsy) && (sy <= whilesy))

Eleganter ist die Verwendung einer Containerklasse   für Symbole, die dann auch die üblichen Mengenoperationen zur Verfügung stellt. Eine Testfunktion könnte dann etwa so aussehen:

void test(setofsys *s1, setofsys *s2, errorrange errnr)

/*tests if sy in s1, gives errormessage if no
  and skips until sy in s1+s2 */

{
 if (!s1->isMember(sy))
 {
  error(errnr);
  s1* = s1* + s2*;
  while(!s1->isMember(sy) || cin.eof()) insymbol;
 }  
}

Zum Abschluß ein Ausschnitt aus der Analysefunktion für Statement aus einem Pascal-Compiler:

            forsy:
    BEGIN
     insymbol;
     IF sy = ident
     THEN insymbol
     ELSE
     test([],fsys + [becomes,tosy,downtosy,dosy],5);
     IF sy = becomes
     THEN
      BEGIN
       insymbol;
       expression(fsys + [tosy,downtosy,dosy])
      END
     ELSE test([],fsys + [tosy,downtosy,dosy],58);
     IF (sy = tosy) OR (sy = downtosy)
     THEN
      BEGIN
       insymbol;
       expression(fsys + [dosy])
      END
     ELSE test([],fsys + [dosy],59);
     testsy(dosy,54);
     statement(fsys,statendsys)
    END;

Die Qualität der Fehlerbehandlung läßt sich weiter verbessern, wenn man die Analysefunktionen etwas flexibler hält, als von der Syntax vorgegeben.

So müssen in OBERON-0 z.B. die Deklarationen in einer festen Reihenfolge (CONST, TYPE, VAR, PROCEDURE) erfolgen:

if (sy == constsy) ...
if (sy == typesy) ...
if (sy == varsy) ...
while (sy == proceduresy) ...

Programme mit Deklarationen lassen sich besser analysieren, wenn man die folgende Konstruktion benutzt:

while ((sy >= constsy) && (sy <= proceduresy))
{
 if (sy == constsy) ...
 if (sy == typesy) ...
 if (sy == varsy) ...
 while (sy == proceduresy) ...
 if ((sy >= constsy) && (sy <= proceduresy))
  error(``Falsche Deklarationsreihenfolge'');
}

Auf diese Weise werden die Deklarationen trotz falscher Reihung vollständig geparst.




Next: Bottom-up-Analyse Up: Syntaktische Analyse Previous: BNF und EBNF

Prof. Dr. Reinhard Völler