Kompilatorer och interpretatorer: Lösningar till tentamen 2025-01-16

Observera att detta är förslag på lösningar. Det kan finnas andra lösningar som också är korrekta. Det kan hända att en del av lösningarna är mer omfattande än vad som krävs för full poäng på uppgiften, eller att de bara hänvisar till var man kan läsa svaren. Dessutom har det inträffat i världshistorien att lärare skrivit fel i lösningsförslagen. Jag har förstås aldrig gjort det, men andra. Är det verkligen någon som läser såna här inledande texter? Jag vet inte. Det kan vara så. Rabarber rabarber rabarber. Det har jag hört att statisterna får säga på filminspelningar när det ska vara bakgrundssorl från en restaurang eller liknande. Här nedan kommer lösningsförslagen till uppgifterna.

Uppgift 1 (3 p)

Vad är skillnaden mellan ett språk, en grammatik och en parser? Hur hänger de ihop?

Svar:

Ett språk är mängden av alla tillåtna meningar ("program", "inmatningar", "sekvenser av tokens") som kan skrivas (eller sägas) på språket.

En grammatik för ett språk är en beskrivning, vanligtvis med någon form av regler, som beskriver vilka meningar som är giltiga i språket. Samma språk kan beskrivas av många olika grammatiker.

En parser är ett datorprogram, eller en del av ett datorprogram, som analyserar inmatningen för att avgöra om den är giltig på ett visst språk. Den matar vanligtvis också ut någon representation av indata, till exempel som en trädstruktur.

Uppgift 2 (4 p)

Här är en grammatik. Den har terminalerna a, b, c, d och e, och icke-terminalerna S, T och U. Startsymbolen är S.

S -> a T | a b U
T -> T c | d
U -> e

Det finns två problem med denna grammatik som gör den olämplig för användning med en LL(1)-parser, som är den typ av prediktiv top-down-parser med en tokens lookahead som vi har sett till exempel i 2.9-programmet. Förklara problemen! Visa gärna var i parserns programkod det blir fel.

Svar:

Det finns en FIRST()-konflikt mellan de två produktionerna för icke-terminalen S:

S -> a T
S -> a b U

I parsern kommer det att finnas en procedur S som man anropar när det ska komma en S i inmatningen. Eftersom en S kan vara antingen a T eller a b U, kommer det att finnas ett val (typiskt en if-sats), och eftersom båda alternativen börjar med a kan man inte veta vilken gren av if-satsen man ska välja om nästa token är a.

Den här produktionen för icke-terminalen T innehåller vänsterrekursion:

T -> T c

I parsern kommer det att finnas en procedur T som man anropar när det ska komma en T i inmatningen. Om man då ska jämföra inmatningen med T c, kommer koden att se ut som T(); match(c);. Man börjar alltså med att anropa proceduren T. Då är man tillbaka till att jämföra inmatningen med T c, och inmatningen har inte ändrats. Då händer samma sak igen, och igen och igen, så parsern har fastnat i en oändlig rekursion.

Uppgift 3 (3 p)

Även om en LL(1)-parser har problem med grammatiken i uppgiften ovan, finns det andra typer av parsers som klarar den. Visa hur en bottom-up-parser med en stack parsar inmatningen a b e.

Svar:

  1. Från början är stacken tom.
  2. Nästa token i inmatningen är a. Eftersom stacken är tom kan vi inte göra något annat än att skifta a, dvs lägga det på stacken. Nu innehåller stacken a.
  3. Nästa token i inmatningen är b. Stacken innehåller a, men det finns ingen produktion som låter oss reducera a, så vi kan inte göra något annat än att skifta b. Nu innehåller stacken a b.
  4. Nästa token i inmatningen är e. Stacken innehåller a b, men det finns ingen produktion som låter oss reducera a b, så vi kan inte göra något annat än att skifta e. Nu innehåller stacken a b e.
  5. Inmatningen är slut, så det finns inget att skifta, utan vi måste reducera. Stacken innehåller a b e. Enligt produktionen U -> e kan vi reducera e till U. Efter att vi reducerat e innehåller stacken a b U.
  6. Inmatningen är förstås fortfarande slut, och vi måste reducera. Stacken innehåller a b U. Enligt produktionen S -> a b U kan vi reducera a b U till S. Nu innehåller stacken S, vilket är startsymbolen, så vi har reducerat inmatningen till startsymbolen och är klara.

Uppgift 4 (9 p)

Det här är ett programavsnitt i ett C-liknande språk:
if (a < b) {
    c = d;
    e = f;
    if (g + h > i + j * k + l) {
        m = n / o / p;
    }
    else {
        q = r;
    }
    s = t * u + v * w * x;
}

Översätt ovanstående programavsnitt till två av följande tre typer av mellankod. (Skulle du svara på alla tre, räknas den med högst poäng bort.)

a) ett abstrakt syntaxträd (genom att rita upp trädet!)

Svar:

Man kan avsluta ";-listorna" med NULL i stället för den sista satsen i listan. Det blir lite mer att rita, men kan vara lite enklare programmeringsmässigt.

Ett (abstrakt) syntaxträd

b) postfixkod för en stackmaskin

Svar:

        rvalue a
        rvalue b
        <
        gofalse AFTER-IF-1
        lvalue c
        rvalue d
        =
        lvalue e
        rvalue f
        =
        rvalue g
        rvalue h
        +
        rvalue i
        rvalue j
        rvalue k
        *
        +
        rvalue l
        +
        >
        gofalse ELSE-START-2
        lvalue m
        rvalue n
        rvalue o
        /
        rvalue p
        /
        =
        goto AFTER-IF-2
label ELSE-START-2:
        lvalue q
        rvalue r
        =
label AFTER-IF-2:
        lvalue s
        rvalue t
        rvalue u
        *
        rvalue v
        rvalue w
        *
        rvalue x
        *
        =
label AFTER-IF-1:

c) treadresskod

Svar:

        if a ≥ b goto AFTER-IF-1
        c = d
        e = f;
        temp1 = g + h
        temp2 = j * k
        temp3 = i + temp2
        temp4 = temp3 + l
        if temp1 ≤ temp4 goto ELSE-START-2
        temp5 = n / o
        m = temp5 / p
        goto AFTER-IF-2
label ELSE-START-2:
        q = r
label AFTER-IF-2:
        temp6 = t * u
        temp7 = v * w
        temp8 = temp7 * x
        s = temp6 + temp8
label AFTER-IF-1:

Scenario till övriga uppgifter

Det finns studenter som låter ChatGPT skriva sina inlämningsuppgifter i stället för att göra dem själva. De lär sig förstås inte så mycket, men de kanske blir klara med labbarna och får betyg och studiemedel.

Tyvärr, ur de studenternas synvinkel, börjar lärarna lära sig att känna igen AI-genererade texter. De texterna har en speciell stil. Till exempel är de nästan alltid rättstavade och grammatiskt korrekta, men meningsbyggnaden kan kännas konstlad eller ovanlig.

Därför behöver de här studenterna ett program som hjälper dem att lura lärarna att deras AI-genererade text egentligen är skriven av studenterna själva! Programmet ska byta ut rättstavade ord mot olika felstavningar, och det ska byta ut fraser (som består av flera ord) mot andra sätt att uttrycka samma sak.

Det vi ska göra här är att skapa ett särskilt inmatningsspråk för att ange vilka utbyten som programmet ska göra. Inmatningsspråket ska innehålla kommandona byt och byt fras, och här är ett exempel på en inmatning:

byt statistik : statestik;
byt krasch : crash krash crasch;
byt fras statistik kan vara ett kraftfullt verktyg : statistik är bra;
byt bransch : branch;
byt millennium : milennium millenium milenium;
byt fras avslöjade snabbt dess begränsningar : funkade inte bra;
klart;

Här har vi bland annat angett att ordet statistik ska bytas mot felstavningen statestik, och att ordet krasch ska bytas mot vilken som helst av felstavningarna crash, krash och crasch. Frasen statistik kan vara ett kraftfullt verktyg ska bytas till statistik är bra. Hela inmatningen ska avslutas med ett klart-kommando, enligt ovan.

Inmatningen kan skrivas på fritt format. Exempelvis är nedanstående inmatning syntaktiskt ekvivalent med den i exemplet ovan.

byt statistik : statestik ; byt krasch : crash krash crasch ; byt fras
statistik kan vara ett kraftfullt verktyg : statistik är bra ; byt

                                        bransch

 : branch ; byt millennium : milennium millenium milenium ; byt
fras avslöjade snabbt dess begränsningar : funkade inte bra ; klart ;

Uppgift 5 (4 p)

a) Vilka terminaler behövs för att man ska kunna skriva en grammatik för språket i scenariot?

Svar:

Kolon, semikolon, ord samt nyckelorden byt, fras och klart.

b) En del av terminalerna har inte fixa lexem, utan kan se olika ut. Skriv, för varje sådan terminal, ett reguljärt uttryck som beskriver hur den får se ut!

Svar:

ord kan beskrivas med det reguljära uttrycket [a-zåäö]+, om man bara vill tillåta gemener ("små bokstäver"). Beroende på vilka språkinställningar man har kan även [a-ö]+ fungera. Tänk på att svenska tecken i reguljära uttryck inte alltid kommer att fungera, och förutom språkinställningarna beror det också på implementationen.

Uppgift 6 (5 p)

Skriv en grammatik för språket. Startsymbolen ska vara inmatning, som representerar en komplett inmatning enligt scenariot ovan.

Svar:

inmatning -> kommandon klartkommando
klartkommando -> klart semikolon
kommandon -> kommandon kommando | ε
kommando -> byt ord kolon flera-ord semikolon | byt fras flera-ord kolon flera-ord semikolon
flera-ord -> flera-ord ord | ord

Som vi kan se är den grammatiken inte fri från FIRST()-konflikter och vänsterrekursion. Om man redan nu vill skapa en grammatik som fungerar på fråga 8, där man ska skriva en prediktiv recursive-descent-parser, kan den se ut så här:

inmatning -> kommandon klartkommando
klartkommando -> klart semikolon
kommandon -> kommando kommandon | ε
kommando -> byt kommandorest
kommandorest -> ord kolon flera-ord semikolon | fras flera-ord kolon flera-ord semikolon
flera-ord -> ord noll-eller-flera-ord
noll-eller-flera-ord -> ord noll-eller-flera-ord | ε

Uppgift 7 (4 p)

Ett parse-träd (ibland kallat "konkret syntaxträd") innehåller noder för alla icke-terminaler. Rita upp parse-trädet för den här inmatningen, enligt din grammatik i uppgiften ovan:

byt sant : sannt ;
byt fras en annan faktor : och sen ;
klart ;

Svar:

Enligt den första grammatiken i uppgiften ovan.

Ett parse-träd

Uppgift 8 (8 p)

Skriv en prediktiv recursive-descent-parser för språket i ett språk som åtminstone liknar C, C++, C# eller Java. Du behöver inte skriva exakt korrekt programkod, men det ska framgå vilka procedurer som finns, hur de anropar varandra, och vilka jämförelser med tokentyper som görs. Du kan anta att det finns en funktion som heter scan, som returnerar typen på nästa token, och en funktion som heter error, som man kan anropa när något gått fel och som skriver ut ett felmeddelande och avslutar programmet.

Svar:

Ladda ner för provkörning: aiparser.c, plus en hjälpfil i form av en Lex-scannerspecifikation, ai.l, och en Bison-version av grammatiken, ai.y. Det finns också en Makefile för att bygga parsern.

Token-typer som scannern antas returnera: BYT FRAS KLART KOLON SEMIKOLON ORD

int lookahead;

void match(int expected) {
    if (lookahead != expected)
        error();
    else
        lookahead = scan();
}

void inmatning(void) {
    kommandon(); klartkommando();
}

void klartkommando(void) {
    match(KLART); match(SEMIKOLON);
}

void kommandon(void) {
    if (lookahead == BYT) {
        kommando(); kommandon();
    }
    else {
        // Ingenting
    }
}

void kommando(void) {
    match(BYT); kommandorest();
}

void kommandorest(void) {
    if (lookahead == ORD) {
        match(ORD); match(KOLON); flera_ord(); match(SEMIKOLON);
    }
    else if (lookahead == FRAS) {
        match(FRAS); flera_ord(); match(KOLON); flera_ord(); match(SEMIKOLON) ;
    }
    else
        error();
}

void flera_ord(void) {
    match(ORD); noll_eller_flera_ord();
}

void noll_eller_flera_ord(void) {
    if (lookahead == ORD) {
        match(ORD); noll_eller_flera_ord();
    }
    else {
        // Ingenting
    }
}

void parse() {
    lookahead = scan();
    inmatning();
    printf("Ok.\n");
}

int main() {
    printf("Skriv en inmatning enligt grammatiken!!\n");
    printf("Avsluta med CTRL-D!\n");
    parse();
}


Thomas Padron-McCarthy (thomas.padron-mccarthy@oru.se) 24 januari 2025