Syntax, syntaxträd, rekursiv medåkning

Syntax för formella språk

Ett formellt språk är en väldefinierad uppsättning textsträngar som kan vara oändligt stor, till exempel alla Java-program, eller ändligt stor, till exempel alla månadsnamn Det bästa sättet att definiera ett språk är med en syntax (observera betoning på sista stavelsen!), det vill säga en grammatik. En så kallad kontextfri grammatik kan beskrivas i Backus-Naur-form (BNF).

Exempel: Språket som består av satserna JAG VET, JAG TROR, DU VET och DU TROR definieras av syntaxen

<Sats> ::= <Subj> <Pred>
<Subj> ::= JAG | DU
<Pred> ::= VET | TROR

I syntaxen ovan har vi tre omskrivningsregler. Varje regel består av ett vänsterled med en icke-slutsymbol (t ex <Sats> ovan) och ett högerled som talar om vad man kan ersätta vänsterledet med. I högerledet får det förekomma både icke-slutsymboler och slutsymboler (t ex JAG i exemplet ovan). Tecknet | betyder eller. Språket består av alla satser som kan bildas genom omskrivningar av startsymbolen <Sats>.

Meningar av typen
JAG VET ATT DU TROR ATT JAG VET OCH JAG TROR ATT DU VET ATT JAG TROR definieras nu så här:

<Mening> ::= <Sats> | <Sats><Konj><Mening>
<Konj>   ::= ATT | OCH
Plötsligt har vi fått en syntax som beskriver en oändlig massa meningar!

Syntaxen för programspråk beskrivs ofta i BNF. Så här kan man visa hur Javas tilldelningssatser ser ut:

<Assignment> ::= <Identifier> = <Expression> ;
<Identifier> ::= <Letter> | <Letter> <Digit> | <Letter> <Identifier>
<Letter>     ::= a-z | A-Z | _ | $
<Digit>      ::= 0-9
Duger denna syntax eller behöver den förbättras?


En kompilator fungerar ungefär så här:

 källkod --> lexikal analys --> syntaxanalys --> semantisk analys --> kodgenerering --> målkod
Den första delen av syntaxanalysen, att kontrollera om ett program följer syntaxen kan göras med rekursiv medåkning eller med en stack.

Rekursiv medåkning (recursive descent)

För varje symbol i grammatiken skriver man en inläsningsmetod. Flergrenade definitioner kräver tjuvtitt med Mio.nextChar(). När något strider mot syntaxen låter vi ett särfall skickas iväg. Om vi vill analysera grammatiken för meningar av typen "JAG VET ATT DU TROR" behöver vi alltså metoderna: Här följer ett program som undersöker om en mening följer syntaxen. Programmet använder en gammal inmatningsklass Mio men det går att förstå ändå.
class SyntaxAnalys{
    
    private Mio mio = new Mio();
    
    public void kontrolleraInmatning(){
	try {
	    läsMening();
	    mio.getLine();
	    System.out.println("Din mening följer syntaxen.");
	}
	catch(SyntaxErrorException e) {
	    System.out.println(e + " före " + mio.getLine());
	}
    } // kontrolleraInmatning()
    
    private void läsMening() throws SyntaxErrorException {
	    läsSats();
	    if (!mio.eoln()) {
		läsKonj();
		läsMening();
	    }
    } // läsMening()

    private void läsSats() throws SyntaxErrorException{
	läsSubjekt();
	läsPredikat();
    } // läsSats()

    private void läsSubjekt() throws SyntaxErrorException{
	String ord = mio.getWord();
	if (ord.equals("JAG") || ord.equals("DU"))
	    return;
	else
	    throw new SyntaxErrorException("Subjektfel");
    } // läsSubjekt()

    private void läsPredikat() throws SyntaxErrorException{
	String ord = mio.getWord();
	if (ord.equals("VET") || ord.equals("TROR"))
	    return;
	else
	    throw new SyntaxErrorException("Predikatfel");
    } // läsPedikat()

    private void läsKonj() throws SyntaxErrorException{
	String ord = mio.getWord();
	if (ord.equals("ATT") || ord.equals("OCH"))
	    return;
	else
	    throw new SyntaxErrorException("Konjunktionsfel");
    } // läsKonj()

} // class Syntax

public class SyntaxErrorException extends Exception {
    public SyntaxErrorException(String errorMessage){
        super (errorMessage);
    }
}//class SyntaxErrorException

Denna metod för syntaxanalys ska ni använda er av i extrauppgift 2. Programmet ska läsa in en matematikformel tecken för tecken och med rekursiv medåkning kolla syntaxen och utföra beräkningen. Parenteser, multiplikation, division, addition och subtraktion ska ingå.

Syntaxkontroll med stack

Ett alternativt sätt att kontrollera om inmatningen följer en syntax är att använda en stack. Som exempel tar vi upp en vanlig källa till kompileringsfel: omatchade parenteser. Så här kan man använda en stack för att hålla reda på parenteserna:
  1. Skapa en tom stack
  2. Slinga som läser symboler (här:tecken) tills inmatningen tar slut
  3. När inmatningen tar slut - kolla om stacken är tom. Om den inte är tom har vi fått ett syntaxfel.
Men vad händer om man lagt in parenteser inuti en kommentar? Vi vill att alla tecken inuti kommentaren ska läsas bort. Lösningen är att låta programmet bete sig som en automat med flera tillstånd.
  1. Letar parenteser att lägga på stacken. Övergår till tillstånd 2 om den upptäcker en kommentar, till tillstånd 3 om inmatningen tar slut.
  2. Inuti kommentaren - läser bort tecken. Återgår till tillstånd 1 om kommentaren tar slut.
  3. När inmatningen tar slut - kollar om stacken är tom. Om den inte är tom har vi fått ett syntaxfel.

Syntaxträd

I exemplen ovan har vi använt syntaxen enbart för att undersöka om en text bryter mot en given syntax. Nästa steg är att tolka texten och bygga upp ett syntaxträd. Uppgift:
Rita först upp ett syntaxträd för uttrycket 2*(3+4*5).
Skriv sedan en BNF-grammatik för vanliga taluttryck med operationerna + - * /. Den vanliga prioritetsordningen (* och / går före + och -) ska gälla mellan operatorerna. Man ska också kunna använda parenteser i taluttrycken.
Skriv till sist ett program som bygger upp syntaxträdet.

Lösning:



     |
     *
   /   \
  2    ( )
        |
        +
      /   \
     3     *
         /   \
        4     5
          

<Uttryck> ::= <Term> | <Term> + <Uttryck> | <Term> - <Uttryck>;
<Term>    ::= <Faktor> | <Faktor> * <Term> | <Faktor> / <Term>;
<Faktor>  ::= TAL | -<Faktor> | (<Uttryck>);
          
Koden för uttryck skulle kunna bli ungefär:
    double readExpr() {
        // expr ::= term | term + expr | term - expr

        double x = readTerm();         // expr ::= term ...

        if (peek('+')) {               // expr ::= term + expr 
            popToken();
            x += readExpr();
        }                              // TODO expr ::= term - expr 

        return x;
    }
Där peek() och popToken() är specialskrivna för att tjuvtitta respektiva plocka ut från inmatningströmmen.

Det finns en ofärdig implemention här som man kan testa addition och multiplikation.