indhold - kennethhansen.net€™xzxzxzxzxzxz’....

168
Indhold 1 Compilerens opbygning 4 1.1 Compilerens funktion ................................ 4 1.2 Faser i compileringen ................................ 4 1.3 Tiny og C- ...................................... 7 2 Leksikalsk analyse 9 2.1 Strenge og sprog .................................. 9 2.2 Regulære udtryk .................................. 10 2.3 Deterministiske endelige automater ........................ 12 2.4 Non-deterministiske endelige automater ...................... 14 2.5 Fra regulært udtryk til NFA ............................ 15 2.6 Fra NFA til DFA .................................. 15 2.7 Fra DFA til regulært udtryk ............................ 19 2.8 Lexer-generatorer .................................. 20 3 Grammatikker 26 3.1 Begrundelse ..................................... 26 3.2 Grammatikker .................................... 27 3.3 En grammatik for aritmetiske udtryk ....................... 30 3.4 En grammatik for Tiny ............................... 31 3.5 Andre specifikationer af grammatikker ...................... 32 4 LL-parsing 36 4.1 Recursive descent .................................. 36 4.2 LL(1)-parseren ................................... 37 4.3 First- og follow-mængder .............................. 40 5 LR-parsing 48 5.1 Bottom-up parsing ................................. 48 5.2 LR(0)-parsing .................................... 50 5.3 SLR(1)-parsing ................................... 56 5.4 LR(1)-parsing .................................... 57 5.5 LALR(1)-parsing .................................. 61 5.6 Parsing af tvetydige grammatikker ........................ 63 6 Det abstrakte syntaks-træ 69 6.1 Parsergeneratorer .................................. 69 6.2 Det abstrakte syntakstræ .............................. 72 6.3 Implementering af AST ............................... 74 6.4 Semantiske handlinger ............................... 76 6.5 Traversering af AST ................................ 77 7 Attribut-grammatikker 81 7.1 Attributter og attribut-grammatikker ....................... 81 7.2 Synthetiserede og nedarvede attributter ...................... 84 7.3 Algoritmer til beregning af attributter ...................... 85 1

Upload: haphuc

Post on 29-Apr-2018

218 views

Category:

Documents


3 download

TRANSCRIPT

Page 1: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Indhold

1 Compilerens opbygning 41.1 Compilerens funktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41.2 Faser i compileringen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41.3 Tiny og C- . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

2 Leksikalsk analyse 92.1 Strenge og sprog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92.2 Regulære udtryk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102.3 Deterministiske endelige automater . . . . . . . . . . . . . . . . . . . . . . . . 122.4 Non-deterministiske endelige automater . . . . . . . . . . . . . . . . . . . . . . 142.5 Fra regulært udtryk til NFA . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152.6 Fra NFA til DFA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152.7 Fra DFA til regulært udtryk . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192.8 Lexer-generatorer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

3 Grammatikker 263.1 Begrundelse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263.2 Grammatikker . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273.3 En grammatik for aritmetiske udtryk . . . . . . . . . . . . . . . . . . . . . . . 303.4 En grammatik for Tiny . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313.5 Andre specifikationer af grammatikker . . . . . . . . . . . . . . . . . . . . . . 32

4 LL-parsing 364.1 Recursive descent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 364.2 LL(1)-parseren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 374.3 First- og follow-mængder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

5 LR-parsing 485.1 Bottom-up parsing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485.2 LR(0)-parsing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505.3 SLR(1)-parsing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 565.4 LR(1)-parsing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 575.5 LALR(1)-parsing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 615.6 Parsing af tvetydige grammatikker . . . . . . . . . . . . . . . . . . . . . . . . 63

6 Det abstrakte syntaks-træ 696.1 Parsergeneratorer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 696.2 Det abstrakte syntakstræ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 726.3 Implementering af AST . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 746.4 Semantiske handlinger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 766.5 Traversering af AST . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77

7 Attribut-grammatikker 817.1 Attributter og attribut-grammatikker . . . . . . . . . . . . . . . . . . . . . . . 817.2 Synthetiserede og nedarvede attributter . . . . . . . . . . . . . . . . . . . . . . 847.3 Algoritmer til beregning af attributter . . . . . . . . . . . . . . . . . . . . . . 85

1

Page 2: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

8 Symboltabellen 898.1 Scope og levetid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 898.2 Implementering af symboltabellen . . . . . . . . . . . . . . . . . . . . . . . . . 918.3 D+ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94

9 Typer og typecheck 1009.1 Simple datatyper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1009.2 Enumererede datatyper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1019.3 Arrays, records og pointere . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1029.4 Objekter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1039.5 Assignments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1049.6 Typecheck . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106

10 Procedurer 10810.1 Hukommelsens organisering . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10810.2 Fuldt statiske køretidsomgivelser . . . . . . . . . . . . . . . . . . . . . . . . . 10910.3 Stak-baserede køretidsomgivelser . . . . . . . . . . . . . . . . . . . . . . . . . 11010.4 Stak-baserede køretidsomgivelser med lokale procedurer . . . . . . . . . . . . . 11310.5 Fuldt dynamiske køretidsomgivelser . . . . . . . . . . . . . . . . . . . . . . . . 11410.6 Parameteroverførselsmekanismer . . . . . . . . . . . . . . . . . . . . . . . . . . 11510.7 RISC- og CISC-processorer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117

11 Mellemrepræsentationen 12111.1 Begrundelse for mellemrepræsentationen . . . . . . . . . . . . . . . . . . . . . 12111.2 Instruktioner i 3AC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12211.3 Udtryk i 3AC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12311.4 Kontrolstrukturer og procedurekald i 3AC . . . . . . . . . . . . . . . . . . . . 12411.5 Datatyper i 3AC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12511.6 Implementering af 3AC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126

12 Kodegenerering 12812.1 RISC-processorer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12812.2 CISC-processorer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12912.3 Javas virtuelle maskine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

13 Registerallokering 13313.1 Ideen bag registerallokering . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13313.2 Levetidsanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13513.3 Opbygning af interferens-grafen . . . . . . . . . . . . . . . . . . . . . . . . . . 13913.4 Farvning af interferens-grafen . . . . . . . . . . . . . . . . . . . . . . . . . . . 141

14 Optimering af kode 15114.1 Overblik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

14.1.1 Foldning af konstanter og konstant-propagation . . . . . . . . . . . . . 15114.1.2 Kopi-propagation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15214.1.3 Elimination af død kode . . . . . . . . . . . . . . . . . . . . . . . . . . 15214.1.4 Fælles deludtryk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15214.1.5 Løkke-invarianter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15314.1.6 Reduktion i styrke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153

Løsninger til udvalgte opgaver 154

2

Page 3: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Litteratur 168

3

Page 4: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

1 Compilerens opbygning

Vi skal i dette skrift se pa, hvorledes en compiler fungerer og pa, hvorledes den kan imple-menteres. Vi starter derfor med en kort oversigt over, hvad en compiler faktisk gør.

1.1 Compilerens funktion

En typisk compiler modtager et program, f.eks. et C++-program, i den ene ende og producereren binær, eksekverbar fil i den anden.

Faktisk gemmer der sig i ovenstaende sætning to formal med en compiler:

1) Compileren skal, for et korrekt kildeprogram, producere den tilsvarende, korrekte ekse-kverbare fil

2) Compileren skal kunne detektere eventuelle fejl i kildekoden, gøre opmærksom pa dissefejl, og undlade at producere en eksekverbar fil, hvis der faktisk var fejl

Ordet ’korrekt’ er her meget vigtigt. Det ville være katastrofalt, hvis compileren bare spistealt det, man fodrede den med og, i tilfælde af fejl, spyttede noget sludder ud igen — det villevære alt for nemt at komme til at anvende den ukorrekte, eksekverbare fil til et eller andet.

Endelig er man naturligvis interesseret i, at resultatet — den eksekverbare fil — er sahurtig, sa effektiv som mulig. Men dette ma siges at være et sekundært mal.

Compiler-teknologi anvendes faktisk ogsa i en lang række af andre typer programmer.Mest nærliggende er det nok at se pa oversættere, dvs. programmer, der oversætter en fil i enrepræsentation til en fil i en anden repræsentation. Saledes indeholder de fleste tekstbehand-lingsprogrammer mulighed for at kunne læse og skrive filer i adskillige filformater. Ofte erdet rimeligt trivielt at transformere tekst fra et format til et andet, men i mange tilfælde farman brug for større eller mindre dele af en compiler. Med udbredelsen af XML vil det blivealmindeligt med programmer, der kan transformere fra et XML-format til et andet. XML haren sa kompliceret struktur, at her har man brug for en parser for at kunne gennemføre dissetransformationer.

Men compilerteori anvendes ogsa i enhver browser. En browser er i virkeligheden at opfattesom en compiler, der modtager et HTML-dokument, og oversætter dette. Outputtet er ikkeeksekverbar kode, men derimod grafik pa en skærm. Brugere af LATEX vil ogsa være vant til,at LATEX modtager en kildekode-fil, som sa oversættes til grafik pa skærmen eller papiret. Ibegge tilfælde anvendes der algoritmer og metodikker fra compilerteorien.

1.2 Faser i compileringen

En compiler er et meget stort og kompliceret program, og for at reducere kompleksiteten erdet en fordel at kunne modularisere compileren. Det viser sig at være nemt at lave denneopdeling, idet compileringsprocessen naturligt kan opdeles i en række faser. Gennem tidernehar man faet analyseret sig frem til følgende faser i compileringen:

4

Page 5: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

1) Leksikalsk analyse

2) Syntaktisk analyse

3) Semantisk analyse

4) Generering af mellemrepræsentationen

5) Optimering af mellemrepræsentationen

6) Generering og optimering af malkoden

leksikalskanalyse

syntaktiskanalyse

semantiskanalyse

gen. afmellemrep

optimering

kodegene-rering

kildekode

tokens

det abstrakte syntakstræ

dekoreret abstrakt syntakstræ

mellemrepræsentation

mellemrepræsentation

målkode

Figur 1

Hver enkelt fase modtager et input og leverer et output — se figur 1 for en grafisk oversigt.Inden vi gennemgar de enkelte faser, skal der defineres nogle begreber:

De tre første faser i compileringen kaldes analysen, eller compilerens front-end. I disse faserundersøges kildekodens opbygning, der tjekkes for en lang række fejl, og hvis de ikke findes,ender vi op med det dekorerede abstrakte syntakstræ, som er en (mere eller mindre) abstraktbeskrivelse af kildekoden.

5

Page 6: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

De tre sidste faser er syntesen eller compilerens back-end. Her genereres, i flere trin,malkoden eller den eksekverbare fil ud fra det dekorerede, abstrakte syntakstræ.

To andre begreber, som det er vigtigt at skelne imellem, er syntaks og semantik. Syntak-sen for en stump kildekode henviser til den ”grammatiske”opbygning af kildekoden, menssemantikken refererer til selve betydningen af kildekoden.

F.eks. har følgende tre sætninger: ”2 + 2 = 4”, ”Summen af tallene 2 og 2 er 4”, ”Manfar resultatet 4, nar man plusser 2 og 2”en meget forskellig syntaks, men semantikken er densamme.

I princippet er der ingen sammenhæng mellem syntaks og semantik, men da det i vir-keligheden er meget svært algoritmisk at kunne finde semantikken af en vilkarlig sætning,vælger man indenfor compilerteorien af arbejde med syntaks-styret semantik. Dette betyder,at man nemt kan afgøre semantikken af et program ud fra syntaksen af programmet samt desemantiske regler for programmeringssproget. Man opnar herved, at betydningen af et pro-gram bliver klar — sa klar, at en computer kan arbejde med den — men prisen, man betaler,er, at programmeringssproget bliver meget ufleksibelt i forhold til daglig tale.

I den første fase af compileringen, den leksikalske analyse, opdeles kildekoden i et antaltokens, og overflødig information smides væk. Et token er en størrelse, som repræsenterer etsymbol, et reserveret ord, en identifier eller lignende. Betragt f.eks. følgende stump Java-kode:

public static void main(String[] args) {

// dette er en kommentar

if (x == 34)

y++;

}

Lexeren vil opdele dette i følgende tokens:

public static identifier(void) identifier(main)

lparen identifier(String) lbracket rbracket

identifier(args) rparen if lparen

identifier(x) equals numeral(34) rparen

identifier(y) increment semicolon rbrace

De fleste tokens svarer til reserverede ord eller symboler i Java, men f.eks. ’increment’ er ettoken, der svarer til symbolkombinationen ’++’. Andre tokens svarer til identifiers — navnepa metoder, variable etc. — eller til literale værdier i programmet, f.eks. tallet 34.

Kommentarer og white spaces som mellemrum, linieskift eller tabulatorspring ignoreres,da de ikke har videre betydning for compileringen.

I den næste fase, den syntaktiske analyse eller parsingen, sættes de forskellige tokens sam-men i det abstrakte syntakstræ. Ideen er her at fa opbygget en todimensional repræsentationaf programmet — finde ud af, hvilke statements der hører til hvilken metode, undersøge,hvornar en løkke-struktur starter og slutter, etc.

Bade den leksikalske og den syntaktiske analyse er blevet undersøgt intenst gennem desidste 50 ar. Dette betyder, at der findes en række sofistikerede algoritmer, som man skalsætte sig ind i. Heldigvis betyder dette ogsa, at disse faser er forstaet sa godt, at de kanautomatiseres . . .

6

Page 7: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

I den semantiske analyse tilføjer vi ekstra information til den abstrakte syntakstræ — videkorerer det. Denne information er af semantisk karakter — vi skal bl.a. have fundet ud af,hvilke variable, der anvendes i programmet, hvilke typer de har, og hvor i programmet de ersynlige. Vi skal ogsa have foretaget det sakaldte typecheck. Her undersøger vi, om samtligeassignments, sammenligninger og metodekald er lovlige – man kan f.eks. ikke pa fornuftig vismultiplicere en boolean med en char.

Efter den semantiske analyse er vi nu færdige med analyse-delen af compileren, og vi hardet dekorerede, abstrakte syntakstræ for et korrekt program — hvis vi da ikke konstateredenogle fejl undervejs.

Næste fase er en oversættelse af det abstrakte syntakstræ til mellemrepræsentationen. Davi i sidste ende skal have genereret noget maskinkode, skal vi pa et eller andet tidspunkt havetransformeret høj-niveau-konstruktioner i kildekoden, som f.eks. en for-sætning, til nogle lav-niveau-konstruktioner, som en CPU faktisk kan udføre, f.eks. sammenligninger og betingedehop. Det viser sig at være smart ikke allerede nu at generere den konkrete maskinkode —dette vil komme til at afhænge alt for meget af mal-CPU’ens særheder. I stedet vælger man attransformere til en form for abstrakt maskinkode, mellemrepræsentationen, som man derefterkan arbejde videre med. Vi skal se pa flere forskellige former for mellemrepræsentation, menisær anvende den sakaldte three adress code.

Dernæst skal mellemrepræsentationen optimeres, og endelig genererer vi den konkrete ma-skinkode, malkoden.

Vi vil dog snyde lidt og ikke generere malkode direkte til vores mal-CPU, men nøjes medat generere assembler-kode. Denne skal sa assembles en ekstra gang.

1.3 Tiny og C-

Vi vil anvende to sprog som eksempel pa, hvorledes man kan implementere en compiler, nemligTiny og C-. Begge sprog er taget fra [Lou].

Tiny er, som navnet antyder, et meget simpelt sprog. I sin syntaks minder det megetom Pascal. I Tiny kan man kun arbejde med hele tal, og der findes ikke procedurer ellerfunktioner. Tiny indeholder kun statements som if, if-else, repeat-until, read, write ogassignments.

Et eksempel pa et Tiny-program er følgende:

read x;

if x > 0 then

fact := 1;

repeat

fact := fact * x;

x := x - 1;

until x = 0;

write fact

end

Sin simpelhed til trods er Tiny komplekst nok til, at det ikke er en helt triviel opgave atskrive en compiler til Tiny.

7

Page 8: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

C- er en simplificeret variant af C. Man kan i C- arbejde med hele tal, arrays af heletal, funktioner, procedurer — ogsa rekursive funktioner og procedurer — lokale og globalevariable. Der er statements som if, if-else og while.

Et eksempel pa et program skrevet i C- er:

int fact( int x ) {

/* recursive factorial function */

if (x>1)

return x*fact(x-1);

else

return 1;

}

void main( void ) {

int x;

x = read();

if (x>0)

write (fact (x));

}

8

Page 9: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

2 Leksikalsk analyse

Vi gar straks i gang med den første fase i compileringen, nemlig den leksikalske analyse.Formalet med den leksikalske analyse er dels at fa selve kildekoden opdelt i tokens: Reser-

verede ord, identifiers, literale strenge og tal-konstanter, symboler etc., og dels at fa fjernetsymboler, der i virkeligheden er irrelevante for compileringen, f.eks. ”white spaces” som mel-lemrum og linieskift, samt kommentarer. Outputtet af denne fase er en sekvens af tokens, somleveres videre til parsingen, compileringens næste fase.

Vi skal i det følgende se pa, hvorledes de forskellige tokens kan specificeres — dette skerved de sakaldte regulære udtryk — og vi skal undersøge de konkrete algoritmer, hvorvedopsplitningen i tokens foregar. Herunder kommer vi ind pa de sakaldte endelige automater.Endelig ser vi pa en sakaldt lexer-generator, JLex, som er et program, der ud fra en liste afregulære udtryk skriver programkoden til en færdig lexer.

Materialet i dette kapitel er at finde i stort set alle bøger om compilerteori, f.eks. [ASU],[Lou] eller [App]. Endvidere indeholder [Sip] en mere formalistisk gennemgang af teorien forregulære udtryk og endelige automater.

2.1 Strenge og sprog

Det grundlæggende begreb indenfor sprogteorien er strengen. Vi skal nu se kort pa, hvorledesstrenge og sprog kan defineres formelt:

Et alfabet er en endelig mængde af symboler. Vi betegner ofte alfabetet ved Σ.

Som regel afhænger valgte af alfabet af, hvilken situation vi er i. Snakker vi om sprogetdansk, sa er alfabetet mængden {a, b, c, ..., x, y, z,æ, ø, a}.

I de fleste programmeringssprog, som f.eks. C++ og Pascal, anvendes alfabetet ASCII — ihvert tilfælde sa længe vi er i den leksikalske analyse. (Under den syntaktiske analyse er voresalfabet lig med mængden af tokens)

En streng x over alfabetet Σ er en endelig sekvens af symboler fra Σ.Den tomme streng er en streng bestaende af 0 symboler og betegnes ofte ε.Længden af strengen x er lig med antallet af symboler i symbolsekvensen x og betegnes

|x|.Konkatenationen af strengene x = a1a2 . . . am og y = b1b2 . . . bn er strengen

x · y = a1a2 . . . amb1b2 . . . bn

Ofte skriver vi xy i stedet for x · y.(I de fleste programmeringssprog, som f.eks. Java, angives konkatenation ved symbolet ’+’.

Vi vælger dog at anvende ’·’ i stedet, idet plus-symbolet ogsa optræder med andre betydningersenere.)

Mængden af strenge over Σ betegnes Σ∗.

Eksempel 1Lad os betragte alfabetet Σ = {a, b}. Nedenfor er angivet nogle strenge fra Σ∗:

aababbbb = a2bab4 , aaaaa = a5 , ε

Bemærk, at vi anvender multiplikativ (eksponentiel) skrivemade for strenge.Længderne af disse tre strenge er henholdsvis 8, 5 og 0.

9

Page 10: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Definition 2Et sprog over alfabetet Σ er en mængde af strenge over Σ, dvs. en delmængde af Σ∗.

Sagt pa en anden made — et sprog bestar af en række strenge, som er lovlige. F.eks.indeholder sproget dansk strengene ’hest’, ’bro’ og ’oversætter’, men ikke strengene ’aslglkahf’eller ’xzxzxzxzxzxz’.

Sprog kan defineres pa adskillige mader — enten direkte, vha. brug af mængdebyggernota-tionen, rekursivt vha. grammatikker, eller vha. regulære udtryk. Vi vil i de følgende kapitlerse nærmere pa disse definitionsmader.

2.2 Regulære udtryk

Vi skal nu se pa regulære udtryk og pa, hvorledes disse kan definere sprog. Vi starter med atbetragte nogle operationer, hvorved vi kan danne nye sprog ud fra gamle:

Definition 3Lad L og M være sprog over alfabetet Σ. Sa defineres

a) Foreningsmængden L ∪M = {x ∈ Σ∗ | x ∈ L eller x ∈M}b) Konkatenationen L ·M = {x ∈ Σ∗ | x kan skrives som lm med l ∈ L og m ∈M}c) Lukningen L∗ = {ε} ∪ L ∪ L2 ∪ L3 . . . =

⋃∞i=0 L

i

Sagt i ord: Ved L ∪M vælger man en streng fra enten L eller M , ved LM tager vi envilkarlig streng fra L og konkatenerer med en vilkarlig streng fra M , og ved L∗ konkatenerervi et vilkarligt antal strenge fra L.

Da sprog er mængder, sa kan vi naturligvis ogsa tage fællesmængder, mængdedifferenserog komplementærmængder, men disse operationer spiller ingen rolle i sprogteorien.

Lad os give et eksempel pa lukningen af et sprog:

Eksempel 4Betragt Σ = {a, b}.

{a, b}∗ er mængden af alle mulige strenge, som kun bestar af a og b.{ab}∗ er mængden {(ab)n | n ∈ N0}

Vi kan nu definere et regulært udtryk ved følgende rekursive definition:

Definition 5Et regulært udtryk R over alfabetet Σ er af en af følgende former:

a) a for et vilkarligt symbol a ∈ Σ

b) ε, den tomme streng

c) R|S, hvor R og S er regulære udtryk

d) R · S, hvor R og S er regulære udtryk

e) R∗, hvor R er et regulært udtryk

10

Page 11: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Ligesom ved aritmetiske udtryk undlader man helst parenteser — dette kræver, at de treoperationer ∗, · og | sættes ind i et operator-hierarki. Vi vælger at lade ∗ have højst prioritet,dernæst kommer ·, og | far sa laveste prioritet. (Sammenlign ∗ med potensopløftning, · medmultiplikation og | med addition).

Eksempel 6Betragt udtrykket a∗(ab|ε)|(ab)∗a∗. Dette er et regulært udtryk, og den nemmeste madeat se dette pa er at opbygge udtrykket fra bunden i et sakaldt syntakstræ. I bladene idette træ star der kun symboler fra Σ eller ε, og i de indre knuder star der enten ∗, ·eller | ; vi taler her om stjerne-, konkatenation- og streg-knuder.

En stjerne-knude har kun en efterfølger, mens de to andre typer indre knuder altidhar to efterfølgere.

Træet er vist pa figur 2.

|

*

.

a

b

a

a

ab

* *

.

..

|

Figur 2

Til et regulært udtryk R kan vi tilknytte et sprog, L(R). Dette er igen defineret rekursivt:

Definition 7a) Hvis R = a, sa er L(R) = {a}b) Hvis R = ε, sa er L(R) = {ε}c) Hvis R = R1|R2, sa er L(R) = L(R1) ∪ L(R2)

d) Hvis R = R1 ·R2, sa er L(R) = L(R1) · L(R2)

e) Hvis R = R∗1, sa er L(R) = L(R1)

Sagt pa en anden made: I et regulært udtryk betyder ’|’ valg, ’·’ konkatenation, og ’*’ etvilkarligt antal gentagelser.

Eksempel 8Nogle regulære sprog over alfabetet Σ = {a, b} er

1) L(a∗ba∗) = {w ∈ Σ∗ | w indeholder netop et b}

11

Page 12: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

2) L((a|b)∗b(a|b)∗) = {w ∈ Σ∗ | w indeholder mindst et b}3) L((a|b)∗aab(a|b)∗) = {w ∈ Σ∗ | w indeholder delstrengen aab}4) L(((a|b)(a|b))∗) = {w ∈ Σ∗ | w indeholder et lige antal karakterer}5) L(((a|b)(a|b)(a|b))∗) = {w ∈ Σ∗ | |w| er et multiplum af 3}6) L(ab|ba) = {ab, ba}7) L(a(a|b)∗a | b(a|b)∗b) = {w ∈ Σ∗ | w begynder og ender med samme symbol}8) L((a|ε)b∗) = L(ab∗|b∗) = {abn | n ≥ 0} ∪ {bn | n ≥ 0}9) L((a|ε)(b|ε)) = {ε, a, b, ab}

2.3 Deterministiske endelige automater

Vi skal nu beskrive genkendelsesmaskiner for regulære udtryk, nemlig de sakaldte endeligeautomater. Endelige automater forekommer i to varianter, deterministiske og non-determini-stiske, og vi beskriver først de deterministiske automater:

Eksempel 9

1 2 3

a

b

a,b

ba

Figur 3

Pa figur 3 ovenfor er vist en deterministisk endelig automat. Den bestar af tre tilstande— hver cirkel angiver en tilstand. Tilstanden 1 er starttilstanden, hvilket man kan se paden indkommende pil fra venstre. Tilstand 2 er en sluttilstand, og den tilsvarende cirkeler derfor fedt optrukket.

Mellem tilstandene er der overgange, symboliseret ved pilene. Til hver overgang er dertilknyttet et symbol, hvilket betyder, at overgangen kun er tilladt, hvis det pagældendesymbol er forrest i den streng, vi er ved at behandle.

Lad os fodre automaten med strengen abbab. Vi starter i tilstand 1, og da det førstesymbol i strengen er et a, udfører vi overgangen fra 1 til 1, hvorved a’et spises, og vi harnu strengen bbab tilbage. Det forreste b fører os over i tilstand 2, og endnu et b fører osfra tilstand 2 til tilstand 2. Den resterende streng er nu ab, og vi gar fra tilstand 2 tiltilstand 3, og det sidste b fører os tilbage til tilstand 2. Da vi ender i en sluttilstand, sahar automaten accepteret strengen abbab.

Vi kan ogsa fodre automaten med aabba. Vi bevæger os sa imellem tilstandene irækkefølgen 1 → 1 → 1 → 2 → 2 → 3, og da vi ikke ender i sluttilstanden 2, saaccepterer automaten ikke strengen aabba.

Lad os angive den formelle definition af en deterministisk endelig automat — forkortetDFA (Deterministic Finite Automaton):

Definition 10En deterministisk endelig automat bestar af størrelserne (Q,Σ, δ, q0, F ) , hvor

Q er en endelig mængde af tilstande

12

Page 13: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Σ er et alfabet

δ : Q× Σ → Q er overgangsfunktionen

q0 ∈ Q er begyndelsestilstanden

F ⊆ Q er mængden af sluttilstande

Automaten virker pa en streng s = a0a1 . . . an af symboler fra Σ saledes: Vi danner ensekvens af tilstande q0, q1, q2, . . . , qn, qn+1 ved

q1 = δ(q0, a0) , q2 = δ(q1, a1) . . . , qn+1 = δ(qn, an)

Automaten accepterer strengen, hvis qn+1 ∈ F .

Vi er specielt interesseret i mængden af de strenge, som accepteres af en given DFA. Somvi senere skal se, kan vi til ethvert regulært sprog konstruere en DFA, som netop acceptererstrengene i sproget, og omvendt er et sprog, som accepteres af en DFA altid et regulært sprog.Regulære sprog (eller regulære udtryk) og endelige automater er derfor i virkeligheden to sideraf samme sag.

Eksempel 11Lad os beskrive DFA’en fra eksempel 9 formelt:

Mængden af tilstande er Q = {1, 2, 3}, og alfabetet er Σ = {a, b}.Det er nemmest at angive overgangsfunktionen ved en tabel:

a b1 1 22 3 23 2 2

Starttilstanden er q0 = 1, og sluttilstandene er F = {2}.Det regulære udtryk, der accepteres af automaten, er givet ved a∗b((aa|ab)∗b∗)∗. Lad

os argumentere for dette:

For at komme fra starttilstanden 1 til sluttilstanden 2 skal vi først gennemløbe etantal a’er og dernæst netop et b. Dette betyder, at vores streng skal starte med a∗b .Vi er nu i sluttilstanden 2, og kan komme tilbage hertil ved enten at tage nogle ’b-ture’eller, via tilstand 3, ’(aa|ab)-ture’. Da disse kan komme i vilkarlig rækkefølge, svarerdette til ((aa|ab)∗b∗)∗ , og tilsammen far vi altsa resultatet a∗b((aa|ab)∗b∗)∗.

Vi skal senere se en generel algoritme til at rekonstruere det regulære udtryk ud fraDFA’en.

Det er ikke altid, at man angiver en overgang fra en tilstand for alle symboler i alfabetet.I dette tilfælde accepteres en streng ikke, hvis man pludselig kommer ud for en overgang, derikke findes:

Eksempel 12Betragt DFA’en pa figur 4. Det ses, at nar automaten først nar ned i tilstanden 4, sakan den aldrig slippe væk igen — vi kalder tilstand 4 for en fælde. Vi kunne derfor ligesagodt anvende automaten pa figur 5. Her er fælde-tilstanden 4 udeladt, og vi far en megetsimplere figur.

13

Page 14: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

1 2 3

a

b

a

b

4

b

a,b

a

Figur 4

1 2 3

b

a

b

a

Figur 5

2.4 Non-deterministiske endelige automater

En variant af den deterministiske endelige automat er den non-deterministiske endelige au-tomat — forkortet NFA (Non-deterministic Finite Automaton). I en NFA kan der være flereforskellige overgange ud fra den samme tilstand med det samme symbol, og der kan tilmedvære overgange, som kan finde sted spontant uden at spise et symbol fra strengen — desakaldte ε-overgange. Som følge heraf kan vi ikke sige, at NFA’en er i en bestemt tilstand,men derimod i en mængde af tilstande samtidigt.

Eksempel 13

1 2 3

a,b

b

a,b

ba,�4

Figur 6

Pa figur 6 er vist en NFA. Den har 4 tilstande, hvoraf 1 er starttilstanden og 4 er ensluttilstand. Det er en non-deterministisk automat, da f.eks. tilstand 1 har to b-overgangeud fra sig, og der er en ε-overgang mellem tilstand 2 og 3.

Lad os fodre denne automat med strengen ababba. Til at begynde med er vi kun istarttilstanden 1, og efter at have spist det første a kan vi kun stadigvæk være i tilstand1. Nu kommer et b, og herefter er vi i tilstand 1 eller 2. Men automaten kan spontantspise et ε — altsa ingenting — og ende i tilstand 3, sa vi er altsa i tilstandene 1, 2 eller3. Endnu et a, og tilstand 1 forbliver tilstand 1, tilstand 2 bliver til tilstand 3, og dader ingen a-overgange er fra tilstand 3, dør denne. Vi er altsa i tilstand 1 eller 3. Sakommer et b, og vi ender i tilstandene 1, 2, 3 og 4. Endnu et b, og vi er igen i 1, 2, 3 og4. Endelig kommer det sidste a, som fører os over i tilstandene 1, 3 eller 4. Da tilstand4 er en sluttilstand, sa accepterede automaten strengen.

14

Page 15: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Vi vil ikke give en formel definition af en NFA, da denne definition nok vil forvirre mere,end den forklarer.

Heldigvis anvender man sjældent NFA’er i praksis — det viser sig, at de er kompliceredeat implementere, og endvidere er udførelsestiden høj: Har automaten m tilstande, sa tagerdet for en streng med længde n tiden O(mn) at se, om strengen accepteres. For en DFA ersituationen meget bedre — her tager det kun tiden O(n). Men NFA’er spiller en vigtig rolleinden for den videre teori.

2.5 Fra regulært udtryk til NFA

Der gælder følgende resultat:

Sætning 14Til enhver NFA findes der en DFA, som accepterer det samme sprog. Et sprog er regu-lært, hvis og kun hvis der findes en DFA, som accepterer det.

Vi vil ikke bevise denne sætning, men i stedet eksemplificere den ved et par eksempler.Gennemfører man faktisk beviset, sa skal man anvende følgende strategi:

a) Beskriv en algoritme, som tildeler en NFA til et regulært udtryk.

b) Beskriv en algoritme, som kan omdanne en NFA til en DFA

c) Beskriv en algoritme, som, givet en DFA, kan fortælle, hvilket regulært udtryk, deraccepteres af denne.

I den første algoritme husker man pa, at et regulært udtryk er defineret rekursivt: a (fora ∈ Σ) og ε er regulære udtryk, og hvis R og S er regulære udtryk, sa er R|S, R ·S og R∗ detogsa.

Det er nemt at konstruere automater for udtrykkene a og ε — se figur 7Antag nu, at vi har konstrueret non-deterministiske automater for udtrykkene R og S

saledes, at disse kun har en sluttilstand. Som figur 7 viser, kan vi konstruere en ny non-deterministisk automat for R|S , for R · S og for R∗.

Eksempel 15Lad os konstruere en NFA for udtrykket (a|b)∗abb. Dette gøres nemt med algoritmen frafør. Nogle af trinene pa vejen er vist pa nedenstaende figur 8:

Den færdige automat er vist nederst pa figur 8. Som det ses, er der masser af ε-overgange, der jo kun er til besvær. Det er derfor en fordel at kunne lave denne NFAom til en DFA.

2.6 Fra NFA til DFA

Nar man skal transformere en NFA til en DFA er det en god ide at huske pa, at en tilstand ien NFA i virkeligheden er en mængde af tilstande. Vi kan saledes lave en NFA om til en DFAved at tage samtlige delmængder af tilstande i NFA’en og lade disse være tilstandene i dennye DFA.

15

Page 16: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

a �

� �

��

R

S

R S

�R

a �

R S|

RS

R*

Figur 7

16

Page 17: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

b

� �

��

a

b

� �

��

a

a b b

a b|

( | )*a b

abb

b

� �

��

a

a b b1 2

3 4

65

7 8 9 10 11

( | )*a b abb

Figur 8

17

Page 18: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Dette viser sig dog ikke at være sa smart, da der for en NFA med n tilstande kommer enDFA med 2n tilstande — og de fleste af disse kommer vi aldrig i berøring med. I stedet foropbygger man langsomt DFA’en tilstand for tilstand.

I denne forbindelse far man brug for begrebet ε-lukning:

Definition 16Betragt en NFA, og en mængde T af tilstande i denne NFA. ε-lukningen ε(T ) defineressom den delmængde af tilstande, som kan nas fra T kun ved ε-overgange.

Eksempel 17Betragt NFA’en fra figur 8. ε-lukningen af mængden {7} er ε({7}) = {2, 3, 5, 7, 8}.

Vi kan nu beskrive, hvorledes man transformerer en NFA til en DFA:Den første tilstand i DFA’en er mængden ε({q0}) — altsa ε-lukningen af starttilstanden.For hver eneste tilstand S = s1, s2, . . . , sn i DFA’en og symbol a i alfabetet finder vi en

(maske ny) tilstand som ε-lukningen P af foreningsmængden δ(s1, a)∪ δ(s2, a)∪ · · · ∪ δ(sn, a).Vi finder altsa mængden af NFA-tilstande, som vi kan na fra S vha. a-overgange, og ε-lukkerdenne.

Samtidigt med, at vi opbygger nye tilstande finder vi ogsa overgangstabellen for DFA’en:Symbolet a fører fra tilstand S til tilstand P ovenfor.

Denne proces fortsætter, indtil vi har fundet overgangstabellen for alle overgange, og derikke opstar nye tilstande.

Eksempel 18Lad os konstruere DFA’en svarende til NFA’en fra figur 6.

Vores starttilstand er A = ε({1}) = {1, 2, 3, 5, 8}. Denne tilstand giver to nye, nemlig

δ(A, a) = ε({4, 9}) = {2, 3, 4, 5, 7, 8, 9} = B

og

δ(A, b) = ε({6}) = {2, 3, 5, 6, 7, 8} = C

Vi fortsætter med B og C:

δ(B, a) = ε({4, 9}) = {2, 3, 4, 5, 7, 8, 9} = B

δ(B, b) = ε({6, 10}) = {2, 3, 5, 6, 7, 8, 10} = D

δ(C, a) = ε({4, 9}) = {2, 3, 4, 5, 7, 8, 9} = B

δ(C, b) = ε({6}) = {2, 3, 5, 6, 7, 8} = C

Vi har en ny tilstand D:

δ(D, a) = ε({4, 9}) = {2, 3, 4, 5, 7, 8, 9} = B

δ(D, b) = ε({6, 11}) = {2, 3, 5, 6, 7, 8} = E

18

Page 19: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Den nye tilstand E tages kærligt under behandling:

δ(E, a) = ε({4, 9}) = {2, 3, 4, 5, 7, 8, 9} = B

δ(E, b) = ε({6}) = {2, 3, 5, 6, 7, 8} = C

Hurra, vi har ikke flere tilstande, og er derfor færdige!Da E er den eneste tilstand, der indeholder den gamle sluttilstand 11, sa er E den

eneste sluttilstand.Selve overgangstabellen for DFA’en er:

a bA = {1, 2, 3, 5, 8} B C

B = {2, 3, 4, 5, 7, 8, 9} B DC = {2, 3, 5, 6, 7, 8} B C

D = {2, 3, 5, 6, 7, 8, 10} B EE = {2, 3, 5, 6, 7, 8, 11} B C

DFA’en er vist pa figur 9.

A Ba

b

b

a

C

ED

aa

b

b

b

a

Figur 9

2.7 Fra DFA til regulært udtryk

Vi skal nu se, hvorledes man kan rekonstruere det regulære udtryk ud fra en DFA (eller enNFA). Metoden illustreres med et eksempel:

Eksempel 19Betragt automaten til venstre pa figur 10. Vi vil finde det tilsvarende regulære udtryk:Først tilføjer vi en ny starttilstand, B, og en ny sluttilstand S, og ε-overgange, sa

automaten accepterer de samme strenge som før.Ideen er nu at fjerne tilstandene 1, 2 og 3 en efter en, og tilsvarende modificere

overgangene, indtil vi kun har B og S tilbage. Vi kan sa aflæse det regulære udtrykdirekte.

Vi fjerner først tilstand 1. Dette kræver, at vi laver nye overgange — eller modifice-rer allerede eksisterende overgange — mellem samtlige tilstande, som er forbundet viatilstand 1. Da B, 2 og 3 sender overgange ind til 1, og 2 og 3 modtager overgange fra 1,sa skal vi altsa modificere overgangene B → 2, B → 3, 2 → 2, 2 → 3, 3 → 2 og 3 → 3.

19

Page 20: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

1 2

3

a

a

a

b

b

b

1 2

3

a

a

a

b

b

b

B

S

2

3

B

S

a

b

aa b|

bb

abba a|

B S

3

a aa b( | )*

a aa b ab b( | )* |( | )( | )*|ba a aa b �

( | )( | )* |ba a aa b ab bb

B S

( ( | )* | )(( | )( | )* | )*(( | )( | )*| )| ( | )*a aa b ab b ba a aa b ab bb ba a aa b a aa b�Figur 10

Der er ingen oprindelig overgang fra B til 2, og den kombinerede overgang fra B via1 til 2 svarer til konkatenationen εa = a. Tilsvarende fas en overgang fra B til 3 medværdien εb = b. Fra 2 til 2 er der allerede overgangen med værdien b. Vi far nu en ny— nemlig fra 2 via 1 til 2. Denne giver værdien aa, og den nye overgang fra 2 til 2 faraltsa værdien aa|b.

Tilsvarende gælder for de tre sidste overgange, og vi far den tredie automat pa figuren.Vi fjerner nu tilstanden 2. Her er der en komplikation, idet der findes en løkke — en

overgang fra 2 til sig selv. Denne giver anledning til et mellemled af formen (aa|b)∗ i denye overgange. En af de nye overgange gar fra B til S, og den far værdien a(aa|b)∗ε =a(aa|b)∗.

Efter at have elimineret tilstand 3 kan vi direkte aflæse, at automaten svarer til detregulære udtryk

(a(aa|b)∗ab|b)((ba|a)(aa|b)∗ab|bb)∗((ba|a)(aa|b)∗|ε) | a(aa|b)∗

Dette kan formentligt omskrives til noget simplere...

Denne algoritme er i praksis af begrænset værdi — som regel ønsker man at konstruere enendelig automat ud fra et givet regulært udtryk, ikke omvendt.

2.8 Lexer-generatorer

I praksis vil man ikke implementere endelige automater for at fa foretaget sin leksikalskeanalyse. I stedet anvender man en sakaldt lexer-generator — et program, som fodres med de

20

Page 21: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

regulære udtryk for ens programmeringssprog, og som genererer source-koden til lexeren.Det mest udbredte program af denne type er Lex, som genererer C-kode. Vi vil dog be-

skæftige os med JLex, som er en Java-udgave af Lex.I JLex anvendes de sædvanlige symboler |, · og ∗, men ogsa nogle flere, som i virkeligheden

er forkortelser:

[abcd] er en forkortelse for det regulære udtryk a|b|c|d[a− z] er en forkortelse for a|b|...|z[a− z, A− Z] er en forkortelse for [a− z]|[A− Z]

R? betyder ε|R, dvs. højst en forekomst af R

R+ betyder RR∗, dvs. mindst en forekomst af R

Rustet med denne notation kan vi nu nedskrive regulære udtryk for forskellige konstruk-tioner:

Eksempel 20Et helt tal i Java bestar som bekendt af mindst et ciffer, samt evt. et foranstillet for-tegn. Dette kan skrives som [’+’,’-’]?[0-9]+ i JLex-skrivemaden. Bemærk, at nar et sym-bol omsluttes af apostroffer, sa er det ikke længere et metasymbol, men skal opfattesbogstaveligt.

En identifier i Java bestar af en række af bogstaver, tal, $ eller (underscore) — dogskal det første symbol være et bogstav. Dette kan skrives som [a− z, A− Z][a− z, A−Z, , $]∗.

Ser vi bort fra den eksponentielle skrivemade, sa kan et reelt tal skrives som et muligtfortegn, en række cifre, et decimalpunktum og en række cifre — dog med den krølle, atder ikke behøver at være noget foran decimalpunktummet eller efter decimalpunktum-met, men der skal dog være mindst et ciffer. Vi er derfor nødt til at dele op i to tilfælde,nemlig hvor der helt sikkert star noget foran decimalpunktummet, og hvor der helt sik-kert star noget efter. Dette giver følgende frygtindgydende regulære udtryk: [’+’,’-’]?([0-9]+’.’[0-9]*—[0-9]*’.’[0-9]+)

Et typisk input til JLex kunne være følgende:

/*Java preamble*/

package Parse;

import ErrorMsg.Error.Msg;

%%

%{

private java_cup.rumtime.Symbol tok(int k, Object value) {

return new java_cup.runtime.Symbol(k, yychar,

yychar+yylength(), value);

}

%}

/*JLex definitions;*/

%function nextToken

%type java_cup.runtime.Symbol

21

Page 22: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

%char

%eofval{

{return tok(sym.EOF,NULL);}

%eofval}

digits =[0-9]+

%%

/*Regular expressions and actions;*/

if {return tok(sym.IF, NULL);}

[a-z][a-z0-9]* {return tok(sym.ID, yytext());}

{digits} {return tok(sym.NUM, new Integer(yytext()));}

({digits}"."[0-9]*)|([0-9]*"."{digits})

{return tok.sym.REAL, new Double(yychar()));}

("--"[a-z]*"\n\)|(" "|"\n"|"\t")’

{}

. {ErrorMsg.error(yychar,"illegal character");}

Her beskrives tokens i et simpelt programmeringssprog, som indeholder det reserverede ordif, identifiers bestaende af et bogstav efterfulgt af en række bogstaver og tal, hele tal (udenfortegn), reelle tal (uden fortegn) samt kommentarer af typen

-- dette er en kommentar

Selve inputtet til JLex er delt op i 4 dele, og delene adskilles fra hinanden vha. linierne”%%”.

Den første del indeholder en sakaldt præambel, nemlig Java-sætninger, som indføres udenændringer i det færdige program før kladde-definitionen. Det vil som regel være import- ogpackage-sætninger.

I den anden del kan man skrive Java-sætninger, som vil blive indført i det færdige programlige efter deklarationen af klassen. Her vælger vi at indføre en hjælpemetode tok. tok returnerertokens i form af objekter fra klassen java cup.runtime.Symbol . Denne klasse genereres afparser-generatoren Cup (som vi stifter nærmere bekendtskab med i kapitel 5), men dissetokens, der sendes videre til parseren, indeholder en int, som fortæller, hvilken type token,der er tale om, et objekt, der kan indeholde ekstra information (navnet pa identifieren ellerværdien af heltallet) samt information om, hvor mange karakterer inde i kildekoden, vorestoken starter og slutter.

Cup genererer en klasse sym for os, der indeholder deklarationer af typen

public static final int IF = 1;

Det er disse, vi anvender til at identificere vores tokens. JLex-variablen yychar fortæller,hvor langt inde i filen vi er, og metoden yylength() fortæller, hvor langt et token vi er vedat identificere. Disse informationer skal bl.a. anvendes til rapportering af eventuelle fejl ikildekoden.

I den tredie del er der forskellige definitioner til brug for JLex selv. F.eks. fortæller vii sætningen "%function nextToken", at den metode, der skal kaldes af parseren for at fadet næste token skal hedde nextToken, mens "%type java cup.rumtime.Symbol" fortæller

22

Page 23: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

returtypen for nextToken. Endelig kan man definere sine egne forkortelser, f.eks. digits for[0-9]+.

Den fjerde og sidste del indeholder en liste over regulære udtryk og tilsvarende handlinger:For hvert regulært udtryk defineres en handling, dvs. en stump Java-kode, der udføres, narvi har genkendt det regulære udtryk. De fleste af disse handlinger er return’s — der sendeset passende token videre til parseren, men ved det næstsidste udtryk, der genkender kom-mentarer, mellemrum og linieskift, er der en tom handling — kommentarer og white spacesignoreres altsa, mens der ved det sidste regulære udtryk — et punktum, der kan matche alt— genereres en fejl.

Rækkefølgen af de regulære udtryk er ikke vilkarlig: Passer en streng pa flere forskelligeudtryk, sa matches det til det øverste udtryk. F.eks. kunne strengen ”if”være bade det reserve-rede ord if eller en identifier if, men her sikrer rækkefølgen, af JLex genkender det reserveredeord.

Yderligere information kan findes i dokumentationen til JLex.

Opgaver

Opgave 2.1 Vi betragter strenge over alfabetet Σ = {a, b}. Skriv regulære udtryk for ne-denstaende sprog:

a) alle strenge, som starter med a og ender med b

b) alle strenge, som indeholder mindst 3 b’erc) alle strenge, som indeholder delstrengen abab

d) alle strenge med ulige længde, og som starter med a

e) alle strenge med ulige længde startende med a, eller med lige længde og startende medb

f) alle strenge med længde højst 4g) alle strenge pa nær aa

h) alle strenge pa nær aa og aaa

i) alle strenge, som begynder med a, og hvor hver anden karakter er et a

j) alle strenge, som indeholder mindst 2 a’er og højst et b

Opgave 2.2 Vi har igen alfabetet Σ = {a, b}. Beskriv sproget frembragt af nedenstaenderegulære udtryk:

a) a(a|b)∗ab) ((ε|a)b∗)∗c) (a|b)(a|b)(a|b)d) a∗b∗

e) (a|b)∗f) a∗|b∗g) (aaa)∗

h) aa|(a|b)∗aaai) (a|ba|bb)(a|b)∗j) (a|bb)(a|b)∗

23

Page 24: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

1 2 3

b

a

b

a

Figur 11

Opgave 2.3 Betragt DFA’en pa figur 11. Hvilke af følgende strenge accepteres af denne au-tomat:

ε, ab, a2b2, abba, a3b2

Opgave 2.4 Opskriv den formelle definition af automaten fra figur 4.Hvilke udtryk accepteres af denne automat?

a

ba

b

a,b

a,b

Figur 12

Opgave 2.5 Hvilket regulært udtryk accepteres af automaten pa figur 12?

b

a

b

a

Figur 13

Opgave 2.6 Hvilket regulært udtryk accepteres af automaten pa figur 13?

Opgave 2.7 Lav DFA’er, som accepterer nedenstaende sprog eller regulære udtryk over alfa-betet {a, b}:

a) Mængden af strenge, hvori ethvert a efterfølges umiddelbart af et b

b) Mængden af strenge, hvori antallet af a’ere og b’ere begge er ligec) aa∗b(a|b)d) Mængden af strenge som indeholder abba som substrenge) ε

f) b∗ab∗ab∗

24

Page 25: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

g) Mængden af alle strenge med længde højst 2

Opgave 2.8 Hvilke af følgende strenge accepteres af NFA’en fra figur 6?

ab, ε, abba, bababa, b3a2, b4ab4

Opgave 2.9 Konverter NFA’en fra figur 6 til en DFA

Opgave 2.10 Anvend de viste metoder til at konstruere en NFA for udtrykket (a|b)∗abb,konvertere denne til en DFA, og endelig genfinde det oprindelige regulære udtryk.

Opgave 2.11 Konstruer en JLex-definition til sproget Tiny fra kapitel 1.

25

Page 26: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

3 Grammatikker

Den vigtigste anvendelse af sprogteori er ved specifikationen af syntaksen for et programme-ringssprog. Denne specifikation foregar normalt vha. en grammatik, hvorfor vi skal studeredisse nærmere.

Vi ser nærmere pa, hvorledes man kan opbygge en parser, dvs. et program, som givet engrammatik og en streng genereret af denne grammatik kan producere parse-træet for strengen.

Der findes to forskellige klasser af parsere: Top-down-parsere og bottom-up-parsere, altefter, hvorledes parse-træet opbygges.

En recursive descent er en top-down-parser, men de sakaldte LL-parsere er meget mereeffektive og lettere at skrive. Bogstaverne LL stor for ’Left Left’ — inputstrengen læses fravenstre til højre, og vi far en venstrederivation af strengen. Vi skal især koncentrere os omLL(1)-parsere, men der findes faktisk ogsa LL(k)-parsere. Tallet k angiver her, hvor mangesymboler i inputstrengen der læses fremad for at kunne afgøre, hvilken produktion, der nuskal anvendes. Top-down-parsere omtales nærmere i kapitel 4.

Blandt bottom-up-parserne finder vi de sakaldte LR-parsere: L, fordi vi igen læser strengenfra venstre til højre, men R for ’Right’ — vi far en højrederivation. Som vi skal se, sa er LR-parsere noget vanskeligere at konstruere, men til gengæld kan de parse meget mere generellegrammatikker end LL-parserne.

Vi gennemgar først den meget simple LR(0)-parser, dernæst SLR(1)-parseren og endeligomtales LR(1)- og LALR(1)-parseren. Disse omtales i kapitel 5.

Endelig ser vi i kapitel 6 pa de sakaldte parser-generatorer, der — ligesom lexer-generato-rerne — genererer selve parseren ud fra en specifikation, sa man slipper for at implementere degustne algoritmer selv. Vi skal ogsa se pa, hvorledes man i praksis implementerer parsetræet.

Noget relevant litteratur er [App], [ASU] og [Lou] — i alle tre bøger beskrives parsingmere eller mindre detaljeret. [Sud] behandler grammatikker og parsing pa et væsentligt meredetaljeret og abstrakt niveau.

3.1 Begrundelse

Lad os, for at begrunde det følgende, introducere grammatikker ved et simpelt eksempel:

Som bekendt kan en sætning opbygges af først et grundled, dernæst et udsagnsord, ogendelig et genstandsled. Et grundled bestar af en artikel (f.eks. ’en’ eller ’et’), et tillægsordog et navneord. Det samme gælder for genstandsleddet.

I vores mini-sprog, som er en meget simpel udgave af dansk, tillader vi kun artiklen ’en’,navneordene ’ost’ og ’mand’, tillægsordene ’gul’ og ’sulten’, og udsagnsordet ’spiser’.

Alle disse regler kan opskrives symbolsk:

sætning → grundled udsagnsord genstandsled

grundled → artikel tillægsord udsagnsord

genstandsled → artikel tillægsord udsagnsord

artikel → en

tillægsord → gul | sulten

navneord → mand | ost

udsagnsord → spiser

26

Page 27: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Her betyder→ ’kan erstattes med’, og | angiver en valgmulighed — ligesom ved de regulæreudtryk.

Lad os prøve at konstruere en sætning i ovenstaende mini-sprog. En sadan konstruktionkaldes en derivation (eller udledning) og foregar ved, at vi starter med hjælpesymbolet sætning.For hvert skridt i derivationen tager vi et hjælpesymbol i den sætning, vi indtil videre haropnaet, og erstatter med højresiden af en af reglerne ovenfor, og vi bliver ved, indtil allehjælpesymboler er væk. For at skelne mellem de generelle regler ovenfor og deres specifikkeanvendelse i derivationen, anvender vi symbolet ⇒ mellem to mellemstationer i derivationen:

sætning ⇒ grundled udsagnsord genstandsled⇒ artikel tillægsord navneord udsagnsord genstandsled⇒ artikel tillægsord navneord spiser genstandsled⇒ en tillægsord navneord spiser genstandsled⇒ en sulten navneord spiser genstandsled⇒ en sulten mand spiser genstandsled⇒ en sulten mand spiser artikel tillægsord navneord⇒ en sulten mand spiser en tillægsord navneord⇒ en sulten mand spiser en tillægsord ost⇒ en sulten mand spiser en gul ost

Indenfor sprogteorien kalder vi ordene ’en’ , ’sulten’ etc., som derivationen slutter ellerterminerer med, for terminaler, mens hjælpesymbolerne navneord osv. kaldes non-terminaler.De generelle regler kaldes produktioner.

Bemærk, at ovenstaende procedure kun siger noget om sætningens syntaktiske gyldighed,ikke noget om semantikken eller betydningen. Saledes kan vi f.eks. producere den grammatiskkorrekte, men absurde, sætning:

en sulten ost spiser en gul mand

3.2 Grammatikker

Den oftest anvendte made at definere et sprog pa er vha. en grammatik:

Definition 21En grammatik bestar af følgende: En mængde Σ af terminaler, en mængde N af non-terminaler med et udvalgt startsymbol S og en mængde af produktioner, dvs. regler afformen:

A→ s med A ∈ N og s ∈ (Σ ∪N)∗

s er altsa her en sekvens af terminaler og non-terminaler.

Eksempel 22En simpel grammatik er følgende: Terminalerne er Σ = {a, b}, non-terminalerne erN = {S,A,B} med startsymbolet S, og produktionerne er

S → AB A→ ε|aA B → ε|bB}(Notationen A→ ε|aA er en forkortelse af de to produktioner A→ ε og A→ aA.)

27

Page 28: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

En grammatik bruges til at producere strenge i Σ∗. Dette foregar ved, at vi starter meden streng bestaende af startsymbolet S. Herefter anvender vi produktioner pa følgende made:Vælg en non-terminal i strengen og en produktion, hvor denne non-terminal star pa venstresi-den. Erstat non-terminalen i strengen med den streng, der star pa produktionens højreside.Fortsæt med dette, indtil strengen kun indeholder terminaler.

En sadan række af produktioner og strenge kaldes en derivation, og vi anvender dobbeltepile, ⇒ mellem hvert skridt i derivationen. Vi anvender endvidere notationen ⇒+ og ⇒∗ forderivationer med mindst et skridt henholdsvis mindst nul skridt.

Eksempel 23Lad os derivere en streng i grammatikken fra eksemplet fra før:

S ⇒ AB ⇒ aAB ⇒ aAbB ⇒ aεbB = abB ⇒ abε = ab

Sproget defineret ved en grammatik defineres som mængden af de strenge i Σ∗, som kanderiveres ved hjælp af grammatikken. En sætning er en ’mellemstation’ i en derivation, dvs.en streng over alfabetet Σ∪N og som optræder i en derivation. I eksemplet ovenfor optrædersætningerne S, AB, aAB, aAbB, abB og ab.

Eksempel 24Lad os bevise, at grammatikken fra før definerer sproget X = {ambm | m,n ≥ 0}.

Lad os betegne det sprog, grammatikken definerer, med Y. Vi skal altsa bevise atX = Y , og dette gøres i to trin: Først ser vi, at X ⊆ Y , og dernæst, at Y ⊆ X.

Vi skal vise, at X ⊆ Y , dvs. at enhver streng i X kan deriveres vha. grammatikken.Men dette kan gøres saledes:

S ⇒ AB ⇒∗ amAB ⇒ amεB = amB ⇒∗ ambnB ⇒ ambnε = ambn

Omvendt skal vi se, at Y ⊆ X. Dette gøres ved at bevise, at enhver sætning igrammatikken er af formen ambn, amAbn, ambnB eller amAbnB. Da kun strenge af denførste form kan være slutresultatet af en derivation, vil dette kunne bevise det ønskede.

Vi viser, at formen af strengene er som ovenfor ved induktion. Efter den første pro-duktion har vi strengen AB, som klart opfylder pastanden. Antag induktivt, at vi har ensætning af formen f.eks. ambnB. De eneste produktioner, vi kan anvende her, er B → bB,som giver sætningen ambn+1B, eller B → ε, som giver sætningen ambn. I begge tilfældefar vi en sætning af den ønskede form.

Vi skal naturligvis ogsa undersøge alle de andre tilfælde, men dette foregar pa sammemade.

To typer af derivationer er særligt interessante: En venstre-derivation er en derivation, hvorproduktionen altid erstatter den non-terminal, der er længst til venstre i strengen, og i enhøjre-derivation erstattes den non-terminal altid, som er længst til højre.

Eksempel 25En venstre-derivation er

S ⇒ AB ⇒ aAB ⇒ aεB = aB ⇒ abε = ab

28

Page 29: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

En højre-derivation af den samme streng er

S ⇒ AB ⇒ AbB ⇒ Abε = Ab⇒ aAb⇒ aεb = ab

Man illustrerer ofte derivationen af en streng med et sakaldt parse-træ. Et sadant træ har enterminal eller non-terminal (eller den tomme streng) i hver enkelt knude, og startsymbolet stai roden. Hver gang vi erstatter en non-terminal, sa tilføjer vi datterknuder til den tilsvarendeknude. Vi tilføjer en knude for hvert symbol i produktionens streng til højre.

Parse-træerne for de to derivationer fra før er ens — se figur 14.

b

S

B

a A

A

B

Figur 14

Nogle grammatikker er tvetydige. Dette betyder, at der kan deriveres strenge fra gramma-tikken, hvor parse-træerne for derivationerne ikke er ens — og betydningen af strengen erderfor uklar. Tvetydige grammatikker er til stort besvær indenfor sprogteorien.

Eksempel 26Et eksempel pa en tvetydighed optræder indenfor de fleste programmeringssprog: Be-tragt pseudo-koden if B1 then if B2 then S1 else S2 hvor B1 og B2 er udsagn og S1

og S2 kommandoer. Hører else med til den første eller den anden if?En grammatik, der kan producere sadanne strenge, er givet ved Σ = {i, t, e, b, s} og

N = {S}, hvor vi for simpelheds skyld forkorter if med i etc. Produktionerne er

S → ibtS | ibtSeS | sog strengen ibtibtses svarende til pseudo-koden fra før kan nu produceres pa to forskelligemader:

S ⇒ ibtS ⇒ ibtibtSeS ⇒∗ ibtibtses

S ⇒ ibtSeS ⇒ ibtibtSeS ⇒∗ ibtibtses

De to parse-træer er ganske forskellige, som det ses pa figur 15.Normalt ville man vælge den første derivation, altsa anvende reglen om, at en else

hører til den if, som star nærmest i programmet.

29

Page 30: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

S

S

S S

i

b

t

s

ei

b

t

s

S

S S

i

b

t

ei

b

t

sS

s

Figur 15

3.3 En grammatik for aritmetiske udtryk

Vi skal nu se, hvorledes man kan lave en grammatik, som frembringer alle aritmetiske udtryk.For simpelheds skyld koncenterer vi os kun om udtryk med addition og multiplikation, ogi stedet for at generere tal eller variable nøjes vi med at anvende terminalen x for disse. Viønsker altsa at kunne generere udtryk som x+x∗x, x∗x∗x, (x+x)∗x og (((x+x)∗x)+x)∗x.

Eksempel 27Som et første forsøg pa at finde en sadan grammatik konstaterer vi, at det fundamen-tale begreb indenfor aritmetikken er et udtryk, og at et udtryk enten er et x, ellersummen eller produktet af to udtryk, eller et nyt udtryk i parenteser. Vi lader derfornon-terminalen E sta for et udtryk, og far derfor produktionerne:

E → x | E + E | E ∗ E | (E)Desværre er denne grammatik utvetydig. Betragt f.eks. de to derivationer:

E ⇒ E + E ⇒ E + E ∗ E ⇒ x+ E ∗ E ⇒ x+ x ∗ E ⇒ x+ x ∗ x

E ⇒ E ∗ E ⇒ E + E ∗ E ⇒ x+ E ∗ E ⇒ x+ x ∗ E ⇒ x+ x ∗ x

Disse genererer den samme streng, x + x ∗ x, men har forskellige parse-træer, som detses pa figur 16.

30

Page 31: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

E

E E+

E E*x

E

E E

E E

*

+

x x x x

x

Figur 16

Som vi senere skal se, vil man fortolke (eller beregne) et parse-træ nedenfra og opefter.Værdien af det venstre parse-træ er derfor x + (x ∗ x), men det til højre fortolkes som(x+ x) ∗ x.

Eksempel 28Løsningen pa tvetydigheden i foregaende eksempel er at indbygge en operatorpræcedens igrammatikken, saledes at multiplikation beregnes før addition — og at udtrykket x+x∗xtolkes som x+ (x ∗ x).

Ser man pa parse-træerne pa figur 16, konstaterer man, at problemet kan løses, hvisgrammatikken udformes, saledes at multiplikations-knuder automatisk skubbes længerened i træet end additions-knuder. Dette kan gøres ved at indføre to nye non-terminalerT (for term) og F (for faktor). Et udtryk bestar derfor af udtryk og termer lagt sammen,en term bestar af termer og faktorer ganget sammen, og en faktor er enten x eller etudtryk i parenteser.

Produktionerne i grammatikken bliver derfor:

E → E + T |T T → T ∗ F |F F → x|(E)

Dette er et generelt princip: Tvetydigheder i grammatikken kan som regel løses ved atindføre flere non-terminaler. Prisen herfor er naturligvis, at grammatikken bliver mere kom-pliceret, og parse-træerne bliver større.

3.4 En grammatik for Tiny

Vi skal nu se pa en grammatik for et programmeringssprog, nemlig for ’legetøjssproget’ Tinyfra kapitel 1. Nar man nedskriver grammatikken for et programmeringssprog, sa vælger manen lidt anden notation end sædvanligt:

• Da man hurtigt løber tør for enkelte bogstaver som non-terminaler, bruger vi nu ord istedet for non-terminaler. Disse vil blive kursiveret.

• Reserverede ord etc. fra sproget repræsenteres jo af tokens fra lexeren. Disse terminalervil blive skrevet med fedt.

31

Page 32: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

• Andre tokens/terminaler er f.eks. identifiers og tal. Disse vil skrives bade med fed ogkursiv.

Vi kommer nu til grammatikken for Tiny:

program → stmt-seq

stmt-seq → statement ; stmt-seq | statement

statement → if-stmt | repeat-stmt | assign-stmt | read-stmt | write-stmt

if-stmt → if exp then stmt-seq end | if exp then stmt-seq else stmt-seq end

repeat-stmt → repeat stmt-seq until exp

assign-stmt → identifier := exp

read-stmt → read identifier

write-stmt → write identifier

exp → simple-exp comparison-op simple-exp | simple-exp

comparison-op → > | < | =

simple-exp → simple-exp addop term | term

addop → + | −term → term multop factor | factor

multop → * | /

factor → ( exp ) | number | identifier

Et par kommentarer: Produktionen stmt-seq → stmt-seq ; statement | statement illustrererstandard-maden for at generere en liste pa vha. grammatikker: Vi ønsker en gentagelse afsymbolet X, og dette gøres ved en produktion af formen A→ AX|X.

Tiny er Pascal-lignende i sin opbygning af statements. Hermed menes, at en if altid af-sluttes med en end etc. Herved undgar vi tvetydighed.

Selve den del af grammatikken, der genererer udtryk er lidt anderledes end i sektion 3.2,idet vi anvender non-terminalerne relop, multop og addop i stedet for direkte at indsætte plus,minus etc. i produktionerne for exp og term. Dette skyldes, at grammatikken herved blivermeget nemmere at vedligeholde og især udvide. Ville vi f.eks. udvide Tiny med en modulus-operator, sa skulle vi blot indføre denne et sted, nemlig i produktionen multop → ∗|/|%.Endvidere bliver det pa denne made meget lettere at gennemskue operator-hierarkiet.

3.5 Andre specifikationer af grammatikker

Vores made at opskrive grammatikker pa benævnes ofte BNF — Backus-Naur form — ogblev udviklet af John Backus og Peter Naur til brug for specifikationen af Algol60. Men derfindes andre skrivemader:

En udvidelse af BNF er EBNF — extended Backus-Naur form. I EBNF inkorporeres genta-gelser og valgmuligheder, lidt ligesom ved operatorerne ∗ og | for regulære udtryk. Dette givernogle mere kompakte og letlæselige grammatikker, men desværre er det tilsvarende sværereat konstruere en parser for dem.

Har vi f.eks. produktionen A → Aα|β , som genererer sætninger af formen βααα . . . , saskriver vi i EBNF A→ β{α} . Som det ses, angiver de krøllede parenteser, at indholdet herikan gentages et vilkarligt antal gange, inklusive 0 gange.

32

Page 33: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Valgmuligheder skrives i EBNF ved kantede parenteser. F.eks. vil produktionen A→ αβ|βi EBNF skrives som A→ [α]β, hvor [ ] sa angiver, at indmaden kan udelades eller tages med,men kun en gange.

Ofte illustrerer man grammatikken vha. de sakaldte syntax diagrammer. Disse bestar afkasser, som repræsenterer non-terminaler, boller, som repræsenterer terminaler, og pile, somforgrener sig og forbinder terminalerne og non-terminalerne. Ideen er sa, at man gar ind idiagrammet fra venstre og følger pilene indtil man slutter til højre. Hver gang man støder paen kasse eller bolle, sa genereres det tilsvarende udtryk.

Lad os f.eks. betragte grammatikken pa BNF-form:

exp → exp addop term | term

addop → + | -

term → term mulop factor | factor

mulop → *

factor → (exp) | number

I EBNF ville den sa saledes ud:

exp → term { addop term }addop → + | -

term → factor {mulop factor}mulop → *

factor → (exp) | number

Syntaks diagrammerne for denne grammatik er vist pa figur 17.

Opgaver

Opgave 3.1 Betragt følgende grammatik: Σ = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, N = {D,S}, S erstartsymbolet og produktionerne er

S → D|DS D → 0|1|2|3|4|5|6|7|8|9

a) Find en venstrederivation af strengen 7801, og tegn det tilsvarende parse-træ.b) Find en højrederivation af strengen 7801, og tegn det tilsvarende parse-træ.c) Beskriv det sprog, grammatikken genererer.

Opgave 3.2 Betragt grammatikken Σ = {a}, N = {S}, S → ε|aaS.Hvilket sprog frembringer denne grammatik?

Opgave 3.3 Betragt grammatikken Σ = {a, b, c}, N = {S,B} , S → a|aBc B → b|bBHvilket sprog frembringer denne grammatik?

Opgave 3.4 Find en grammatik, som genererer sproget {b2n | n ≥ 0}.

Opgave 3.5 Find en grammatik, som genererer sproget {a, ba, bba, bbba, . . . }.

33

Page 34: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

term

term term

exp

-

+addop

factor

factor multop

term

*addop

number

+addop

+exp

Figur 17

34

Page 35: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Opgave 3.6 Find en grammatik, som genererer sproget {ε, ab, abab, ababab, abababab, . . . }.

Opgave 3.7 Betragt grammatikken Σ = {(, )}, N = {S}, S → S(S)S|ε.a) Vis, at denne grammatik netop frembringer alle sekvenser af matchende parenteser.b) Vis, at denne grammatik er tvetydig.c) Find en utvetydig grammatik, som frembringer det samme sprog.

Opgave 3.8 Omskriv grammatikken for Tiny til EBNF.

35

Page 36: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

4 LL-parsing

En vigtig type af parsere er de sakaldte top-down-parsere. Her opbygges, som navnet antyder,parse-træet fra roden og nedefter. Især to typer af top-down-parsere skal fremhæves her,nemlig recursive descent og LL(1)-parsing. I virkeligheden er der tale om to varianter af densamme fundamentale algoritme.

Uheldigvis er top-down-parsere ikke sa rare at arbejde med i praksis — som vi skal se ikapitel 6 er der problemer med at kunne generere parse-træet pa en elegant made. Ydermereer det ikke alle grammatikker, der kan LL(1)-parses.

4.1 Recursive descent

Vi skal nu skrive en parser for grammatikken for de aritmetiske udtryk fra sidste kapitel. Vivælger den simplest mulige parser, en recursive descent parser (eller rekursiv nedstigning).En sadan parser anvender, som navnet antyder, rekursive metodekald i stort tal.

Recursive descent er ikke særligt udbredt, da det kun er meget fa grammatikker, der kanbehandles pa denne made. Bl.a. kan grammatikker med venstre-rekursion ikke parses med enrecursive descent parser, da vi ved en venstre-rekursiv produktion risikerer at fa en uendeligrekursion.

Definition 29En grammatik er venstre-rekursiv, hvis der findes en non-terminal A og en derivationA⇒∗ Aα.

Eksempel 30Idet recursive descent-parsing kræver, at grammatikken ikke er venstre-rekursiv, om-skriver vi grammatikken fra sidste kapitel:

E → E + T |T T → T ∗ F |F F → x|(E)Vi ser, at der er venstre-rekursion bade ved E og ved T .

Følgende trick kan fjerne (umiddelbar) venstre-rekursion:Har vi produktionerne , hvor A → Aα|β og β ikke starter med A, sa kan disse

omskrives, ved indførelse af en ny non-terminal A′, til produktioner, der ikke er venstre-rekursive:

A→ βA′ A′ → αA′|εGrunden til, at dette virker, er at de oprindelige produktioner genererer sætninger afformen βαn, for n ≥ 0 — der kommer et α pa, hver gang reglen A → Aα udføres,og til sidst skal vi anvende produktionen A → β. Men den modificerede grammatikfrembringer nøjagtigt de samme sætninger, sa omskrivningen er gyldig.

Anvender vi denne omskrivning pa grammatikken for aritmetiske udtryk, sa fas:

E → TE ′ E ′ → +TE ′|ε T → FT ′ T ′ → ∗FT ′|ε F → x|(E)— og det er denne grammatik, vi vil arbejde videre med.

36

Page 37: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Ideen bag recursive descent (og for den sags skyld LL(1)-parseren) er at lade parseren sedet næste token i inputstrengen, og ud fra dette kendskab beslutte, hvilken produktion, dernu skal anvendes. Dette næste, synlige token kalder vi et lookahead.

I recursive descent-parseren har vi en metode fra hver non-terminal. Indholdet af dissemetoder er en simpel switch -sætning, eller en række if-else-sætninger, hvori vi, ud fralookahead’et afgører, hvilken produktion der skal genkendes. I hver enkelt case, svarende tilhver enkelt produktion med non-terminalen pa venstresiden, er der dels kald af andre non-terminal-metoder, dels kald af en metode accept, som optræder for alle terminaler. Disse kaldfølger nøje højresiden af produktionen.

Metoden accept modtager et token, sammenligner det med lookahead, og hvis de to er ens,sa er alt vel, og lookahead rykker en token frem i inputstrengen. Passer parametren i acceptikke med lookahead, eller er der ingen case’s, der matcher, sa har vi en fejl, og en metodeerror kaldes.

Saledes far vi til non-terminalen E metoden

private void E() {

System.out.println("E -> TE’");

T();

EPrime();

}

hvor vi, for at se, om parseren faktisk virker, udskriver den tilsvarende produktion.Ved ε-produktioner ser tingene lidt anderledes ud, idet vi her bare skal have non-terminalen

til at forsvinde. F.eks. bliver koden for E ′ :

private void EPrime() {

if (lookahead == ’+’) {

System.out.println("E’ -> +TE’");

accept(’+’);

T();

EPrime();

}

else

System.out.println("E’ -> epsilon");

}

Den fuldstændige kode kan findes i filen RecursiveDescentParser.java .Bemærk, at det ikke er alle grammatikker, der kan parses vha. recursive descent. F.eks. far

vi problemer ved produktioner af typen A → aa|ab: Parseren kan ikke i metoden A med etlookahead pa a vide, om den skal vælge den ene eller den anden produktion. Endelig er der,som sagt, problemer med venstre-rekursion.

4.2 LL(1)-parseren

Vi skal nu se pa en type af top-down parsere, som ikke anvender rekursion, nemlig de sakaldteLL(k)-parsere. Forkortelsen ’LL’ star for ’Left, Left’ betydende, at vi parser inputstrengen fravenstre imod højre, og at vi far en venstre-derivation ud af parsingen, mens tallet k angiver,

37

Page 38: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

hvor langt ind i inputstrengen, parseren kan se, og ud fra disse k tokens beslutte, hvilkenproduktion, der skal anvendes.

Den simpleste, og oftest forekommende, LL-parser er LL(1)-parseren. Her angiver 1-tallet,at parseren læser et symbol frem i inputstrengen, før den bestemmer, hvilken produktion, derskal anvendes.

LL(1)-parser

Parse-tabel, M

Stak

Input

Output

a + b #

X

Y

Z

#

Figur 18

LL(1)-parseren er vist skematisk pa figur 18. Parseren benytter sig af en stak og en parse-tabel. Parseren fungerer i store træk ved, at den sammenligner det næste symbol i inputtetmed det øverste symbol pa stakken. Alt efter hvad denne sammenligning giver, gør parserenet eller andet. En typisk handling er at sla op i tabellen; heri finder parseren en produktion,hvorefter der pushes en række symboler pa stakken. Andre handlinger er at poppe fra stakken,ga et symbol længere frem eller generere en knude i parse-træet.

I virkeligheden er der ikke den store forskel mellem LL(1)-parseren og recursive descent-parseren. Ved recursive descent anvender vi computerens stak til at placere activation records,som svarer til non-terminaler, mens LL(1)-parseren har en explicit stak. Valget mellem, hvilkenproduktion, der skal anvendes, foretages ved recursive descent i en switch-sætning, mensLL(1)-parseren slar op i parse-tabellen i stedet.

For at forklare LL(1)-parse-algoritmen anvender vi nedenstaende pseudo-kode. Vi har en’pointer’ (lookahead) ind i inputstrengen, som er forstærket med symbolet ’$’ i enden. Vi harendvidere en stak, som bestar af symboler (terminaler, non-terminaler og $), og vi kan pushe,poppe og peeke pa denne stak. (Vi minder læseren om, at en stak er en datastruktur, hvorpaman kun kan se det øverste element. peek betyder, at vi aflæser det øverste symbol i stakkenuden at fjerne det, mens pop angiver, at vi fjerner symbolet helt fra stakken. Ved operationenpush(x) placerer vi objektet x øverst pa stakken). Endelig har vi en tabelM , som er indekseret

38

Page 39: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

med to variable — første index er en non-terminal, mens andet index er en terminal eller $.

Stack s;

Symbol x = S; // S er startsymbolet

lad lookahead pege pa starten af inputstrengen;

push($); push(S); // S er startsymbolet

while (x != $){ // stakken er ikke tom

x = s.peek();

if ( x er en terminal eller $ ) {

if ( x == lookahead ) {

x = s.pop();

flyt lookahead et symbol fremad;

}

else error();

else // x er en non-terminal

if ( M[x,lookahead] er produktionen ) {

x = pop();

push (); ... push(); push(); push();

udskriv produktionen ;

}

else // M[x,lookahead] indeholder error

error();

}

}

Bemærk, at symbolerne i hver enkelt produktion pushes pa stakken i omvendt rækkefølge.

Eksempel 31Lad os illustrere denne algoritme. Vi anvender grammatikken for aritmetiske udtryk fraeksemplet ovenfor:

E → TE ′ E ′ → +TE ′|ε T → FT ′ T ′ → ∗FT ′|ε F → x|(E)Som vi senere skal se, sa er parse-tabellen for denne grammatik givet ved:

x + * ( ) $E E → TE ′ E → TE ′

E ′ E ′ → +TE ′ E ′ → ε E ′ → εT T → FT ′ T → FT ′

T ′ T ′ → ε T ′ → ∗FT ′ T ′ → ε T ′ → εF F → x F → (E)

hvor alle blanke felter i tabellen indikerer en fejl, dvs. kald af metoden error.Lad os se, hvorledes denne parser kan behandle udtrykket x+ x ∗ x. I nedenstaende

tabel er vist stakkens tilstand, den del af inputstrengen, vi mangler at læse samt deproduktioner, der udskrives. Hver enkelt linie svarer til situationen lige før et gennemløbaf do-løkken i algoritmen:

39

Page 40: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

stak input output$E x+ x ∗ x$ E → TE ′

$E ′T x+ x ∗ x$ T → FT ′

$E ′T ′F x+ x ∗ x$ F → x$E ′T ′x x+ x ∗ x$ (accept x)$E ′T ′ +x ∗ x$ T ′ → ε$E ′ +x ∗ x$ E ′ → +TE ′

$E ′T+ +x ∗ x$ (accept +)$E ′T x ∗ x$ T → FT ′

$E ′T ′F x ∗ x$ F → x$E ′T ′x x ∗ x$ (accept x)$E ′T ′ ∗x$ T ′ → ∗FT ′

$E ′T ′F∗ ∗x$ (accept *)$E ′T ′F x$ F → x$E ′T ′x x$ (accept x)$E ′T ′ $ T ′ → ε$E ′ $ E ′ → ε$ $ (accept $)

Vi har altsa venstrederivationen:

E ⇒ TE ′ ⇒ FT ′E ′ ⇒ xT ′E ′ ⇒ xE ′ ⇒ x+ TE ′ ⇒ x+ FT ′E ′ ⇒ x+ xT ′E ′

⇒ x+ x ∗ FT ′E ′ ⇒ x+ x ∗ xT ′E ′ ⇒ x+ x ∗ xE ′ ⇒ x+ x ∗ x

som fas ved at tage output-produktionerne en efter en.Bemærk stakkens rolle i ovenstaende: Stakken bruges til at huske pa den del af den mid-

lertidige streng i derivationen, som star efter den non-terminal, vi er i færd med at bearbejde.Saledes har vi i tabellens 9. række situationen, hvor stakken indeholder E ′T ′F , og vores mid-lertidige streng i derivationen er x+FT ′E ′. (Bemærk, at indholdet af stakken star i omvendtrækkefølge af indholdet i strengen).

4.3 First- og follow-mængder

For at kunne opskrive parse-tabellen skal vi have beregnet nogle hjælpestørrelser, nemlig denboolske størrelse nullable samt first- og follow-mængderne.

first- og follow-mængder er helt centrale i teorien for LL-parsere, men spiller ogsa en vigtigrolle i teorien for LR-parsere.

I det følgende arbejder vi med forstærkede strenge, dvs. alle strenge ender med hjælpesym-bolet $, som derfor opfattes som en terminal.

nullable er en boolsk størrelse, som kun antager værdien true eller false. Den er defineretfor hvert eneste non-terminal i grammatikken. first-mængderne er defineret for hvert enestesymbol, terminal savel som non-terminal, i grammatikken og er en mængde af terminaler.follow-mængderne er kun defineret for non-terminaler og er en mængde af terminaler.

Definition 32En non-terminal X i grammatikken er nullable, hvis der findes en derivation X ⇒∗ ε.

40

Page 41: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Kort sagt, en non-terminal X er nullable, hvis den kan forsvinde pa en eller anden made ien derivation uden at efterlade sig spor.

Nullable beregnes iterativt: For alle non-terminaler med ε-produktioner sættes nullable tiltrue. Derefter gennemløbes alle produktionerne en efter en, og hvis alt pa højresiden af enproduktion er nullable, sa er non-terminalen pa venstre-siden ogsa nullable. Disse gennemløbfortsættes gennem alle produktionerne adskillige gange, indtil situationen har stabiliseret sig.De non-terminaler, som ikke har nullable true, ma derfor have nullable false.

For et symbolX er first(X) mængden af de terminaler, som kan komme først i en streng, derderiveres i grammatikken ud fra sætningen X. Hvis X er en terminal, sa er first(X) = {X},hvilket ikke er sa interessant, men hvis X er en non-terminal, sa angiver elementerne i first(X)de terminaler, der kan sta længst til venstre som blade i det deltræ af parse-træet, hvori Xer rod.

Definition 33For ethvert symbol X i grammatikken defineres mængden first(X) som mængden afterminaler, der udregnes vha. nedenstaende:

a) Hvis X er en terminal, sa er first(X) = {X}b) Hvis X er en non-terminal med produktionen X → Y1Y2 . . . Yn, sa er first(Y1)

indeholdt i first(X).

c) Hvis X er en non-terminal med produktionen X → Y1Y2 . . . Yn, og Y1, Y2, . . . Yi−1

er nullable sa er first(Yi) indeholdt i first(X).

Begrundelsen for reglerne b og c er følgende: Hvis non-terminalen Y1 kan derivere til a . . . ,sa ma terminalen a ligge i first(Y1). Men vi kan da fa X til at blive til noget startende med aved derivationen

X ⇒ Y1Y2 . . . Yn ⇒ ∗a . . . Y2Y3 . . . Yn

Tilsvarende, hvis Y1 er nullable, og b ∈ first(Y2), sa kan vi fa X til at derivere til noget, derstarter med b ved at erstatte X med Y1Y2 . . . Yn og dernæst lade Y1 forsvinde, daY1 jo ernullable, og endelig lade Y2 udvikle sig til noget, der starter med b.

I praksis beregnes first-mængder iterativt: Vi beregner først first-mængderne for alle ter-minaler vha. regel a) — dette ma anses for at være ganske trivielt. Dernæst gennemløbessamtlige produktioner, og first-mængderne for non-terminalerne opdateres tilsvarende. Dissegennemløb gentages, indtil first-mængderne ikke ændres.

Eksempel 34Lad os finde nullable og first-mængderne for grammatikken for aritmetiske udtryk fraeksempel 30:

Det ses straks, at E ′ og T ′ er nullable, mens hverken E, T eller F er det — alleF -produktionerne indeholder terminaler, sa F kan aldrig forsvinde, alle T -produktionerindeholder et F , og T kan derfor heller ikke forsvinde, og alle E-produktioner indeholderet T , som ikke kan forsvinde.

Vi far umiddelbart:first(+) = {+} first(∗) = {∗} first(() = {(}first()) = {)} first(x) = {x} first($) = {$}

41

Page 42: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

first-mængderne for non-terminalerne kan nemt og overskueligt beregnes ved ud forhver produktion at opskrive den nye, opdaterede first-mængde i nedenstaende skema.Bemærk, at produktioner, hvor højre-siden starter med en terminal, kun behøver atblive taget i betragtning en gang — det er de nullable non-terminaler, der gør livet surt.

Skemaet skal læses fra venstre mod højre:

E → TE ′ ⇒ ingen ændring af first(E)E ′ → +TE ′ ⇒ + ∈ first(E ′)E ′ → ε ⇒ ingen ændring af first(E ′)T → FT ′ ⇒ ingen ændring af first(T )T ′ → ∗FT ′ ⇒ ∗ ∈ first(T ′)T ′ → ε ⇒ ingen ændring af first(T ′)F → x ⇒ x ∈ first(F )F → (E) ⇒ (∈ first(F )

Dvs. vi har efter første iteration:

first(E) = first(T ) = ∅ first(E ′) = {+} first(T ′) = {∗} first(F ) = {(, x}Fremefter er det kun produktionerne E → TE ′ og T → FT , der kan gøre mere, da alleandre produktioner enten starter med en terminal eller er ε-produktioner.

Vi itererer igen: Produktionen E → TE ′ giver ingen ændring af first(E), men T →FT giver, at (, x ∈ first(T ).

Endnu en iteration viser, at (, x ∈ first(E), og herefter stabiliseres situationen.Resultatet var altsa:

first(E) = first(T ) = first(F ) = {(, x} first(E ′) = {+} first(T ′) = {∗}

follow-mængderne er kun defineret for hver non-terminal og angiver de terminaler, som kankomme umiddelbart til højre for non-terminalen i en eller anden derivation.

Definition 35For hver non-terminal X defineres mængden follow(X) som mængden af de terminaler,der kan komme lige efter X i en sætning i grammatikken. Reglerne er:

a) $ ∈ follow(S), hvor S er startsymbolet i grammatikken

b) Hvis der er en produktion A→ αBβ, sa er first(β) ⊆ follow(B)

c) Hvis der er en produktion A→ αB eller en produktion A→ αBβ med β nullable,sa er follow(A) indeholdt i follow(B)

Begrundelsen for regel a er simpel: Idet vi har forstærket grammatikken med symbolet ’$’,sa vil ’$’ følge efter enhver streng i sproget.

Tilsvarende ses gyldigheden for regel b: Hvis strengen β af terminaler og non-terminaler kanudvikle sig til noget, der starter med a, sa ligger a i first(β). Men vi kan under en produktionkomme ud for, at delstrengen Bβ optræder, og ved at udvikle β til a . . . , kan vi fa Ba . . . , ogderfor ma a ∈ follow(B).

Endelig gælder regel c, for hvis vi har a ∈ follow(A), sa kan delstrengen Aa optræde i enderivation, og ved at anvende produktionen A → αB (eller A → αBβ og dernæst fa β til atforsvinde), kan vi fa frembragt strengen αBa, og a ligger derfor i follow(B).

42

Page 43: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Igen kan vi beregne follow-mængderne iterativt — vi initialiserer ved at anvende reglernea) og b), og gennemløber alle produktioner og anvender regel c), indtil alle follow-mængderneer uændrede.

Iterationerne bliver lidt lettere ved at observere, at ε-produktioner, produktioner kun medterminaler pa højre-siden, eller produktioner med en terminal yderst til højre ingen rolle spilleri disse iterationer.

Eksempel 36Lad os beregne follow-mængderne for grammatikken for aritmetiske udtryk. Regel a)giver umiddelbart, at $ ∈ follow(E), mens regel b) fortæller:

E → TE ′ ⇒ first(E ′) ⊆ follow(T ), dvs. + ∈ follow(T )E ′ → +TE ′ ⇒ first(E ′) ⊆ follow(T ), dvs. + ∈ follow(T )E ′ → ε ⇒ ingen ændringerT → FT ′ ⇒ first(T ′) ⊆ follow(F ), dvs. ∗ ∈ follow(T )T ′ → ∗FT ′ ⇒ first(T ′) ⊆ follow(F ), dvs. ∗ ∈ follow(T )T ′ → ε ⇒ ingen ændringerF → x ⇒ ingen ændringerF → (E) ⇒ first(′)′) ⊆ follow(E), dvs. ) ∈ follow(T )

Resultatet er

follow(E) = {$} follow(T ) = {+} follow(F ) = {∗} follow(E ′) = follow(T ′) = ∅I de følgende iterationer er det kun reglerne E → TE ′, E ′ → +TE ′ , T → FT ′ ogT ′ → ∗FT ′ , der kan anvendes:

E → TE ′ ⇒ follow(E) ⊆ follow(E ′), dvs. $, ) ∈ follow(E ′)E → TE ′ ⇒ da E ′ er nullable, er follow(E) ⊆ follow(T ), dvs. $, ) ∈ follow(T )E ′ → +TE ′ ⇒ follow(E ′) ⊆ follow(E ′), dvs. intet nytE ′ → +TE ′ ⇒ da E ′ er nullable, er follow(E ′) ⊆ follow(T ), men intet nytT → FT ′ ⇒ follow(T ) ⊆ follow(T ′), dvs. + ∈ follow(T ′)T → FT ′ ⇒ da T ′ er nullable, er follow(T ) ⊆ follow(F ), dvs. + ∈ follow(F )T ′ → ∗FT ′ ⇒ follow(T ′) ⊆ follow(T ′), dvs. intet nytT ′ → ∗FT ′ ⇒ da T ′ er nullable, er follow(T ′) ⊆ follow(F ), dvs. + ∈ follow(F )

Resultatet er

follow(E) = follow(E ′) = {$, )} follow(T ) = {+, ), $}follow(T ′) = ∅ follow(F ) = {∗,+}

Endnu en iteration giver:

E → TE ′ ⇒ follow(E) ⊆ follow(E ′), dvs. $, ) ∈ follow(E ′)E → TE ′ ⇒ da E ′ er nullable, er follow(E) ⊆ follow(T ), dvs. $, ) ∈ follow(T )E ′ → +TE ′ ⇒ follow(E ′) ⊆ follow(E ′), dvs. intet nytE ′ → +TE ′ ⇒ da E ′ er nullable, er follow(E ′) ⊆ follow(T ), men intet nytT → FT ′ ⇒ follow(T ) ⊆ follow(T ′), dvs. +, ), $ ∈ follow(T ′)T → FT ′ ⇒ da T ′ er nullable, er follow(T ) ⊆ follow(F ), dvs. +, ), $ ∈ follow(F )T ′ → ∗FT ′ ⇒ follow(T ′) ⊆ follow(T ′), dvs. intet nytT ′ → ∗FT ′ ⇒ da T ′ er nullable, er follow(T ′) ⊆ follow(F ), dvs. +, ), $ ∈ follow(F )

43

Page 44: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

med resultatet:

follow(E) = follow(E ′) = {$, )}

follow(T ) = follow(T ′) = {+, ), $} follow(F ) = {∗,+, ), $}

Vi ser endvidere, at flere iterationer ikke vil ændre dette.

Efter lidt træning vil man ikke være sa detaljeret i sine beregeninger, men i stedet anvendeet skema som nedenstaende:

nullable first followE false (,x ),$E ′ true + ),$T false (,x +,),$T ′ true ∗ +,),$F false (,x +,∗,),$

Her udfylder man først nullable-kolonnen, dernæst first-kolonnen og endelig follow-kolonnen.Vi kan nu endelig opskrive parse-tabellen. Ideen i denne er, at vi til hvert par (X, a),

hvor X er en non-terminal pa stakken og a er det næste symbol i inputtet, finder netop denproduktion, som er første skridt i rækken af produktioner fra X til en streng startende meda.

Har vi derfor en produktion A → α, sa skal denne produktion sættes ind i tabellen allesteder, hvor A kan lede til terminalen a, dvs. produktionen sættes ind i tabellen pa pladsM [A, a] for alle a ∈ first(α).

Endvidere kan vi jo komme ud for, at non-terminalen A bliver ε ved produktionen A→ α.Dette kan kun ske, hvis A er nullable. I sa tilfælde skal a ∈ follow(A), og vi skal sætteproduktionen A→ α ind i cellen M [A, a].

Lad os opsummere:

Definition 37Parse-tabellen M udfyldes saledes:

a) Produktionen A→ α skrives i alle celler M [A, a] for alle a ∈ first(α).

b) Produktionen A → α, med α nullable, skrives i alle celler M [A, a] for alle a ∈follow(A).

c) I alle tomme celler skrives der error

Hvorfor nu det? Jo, hvis vores næste non-terminal er A, og vores lookahead ligger i first(α),sa vil det være en god strategi for parseren af udføre produktionen A→ α for at komme videre.

Alternativt, hvis a ∈ follow(A), sa er en mulighed for parseren at anvende produktionenA→ α — hvis α ellers er nullable — for at slippe med A pa stakken og komme videre.

Eksempel 38Parse-tabellen for grammatikken for de aritmetiske udtryk bliver:

44

Page 45: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

x + * ( ) $E E → TE ′ E → TE ′

E ′ E ′ → +TE ′ E ′ → ε E ′ → εT T → FT ′ T → FT ′

T ′ T ′ → ε T ′ → ∗FT ′ T ′ → ε T ′ → εF F → x F → (E)

Nærlæser man konstruktionen af parse-tabellen, sa opdager man, at der intet er til athindre, at et felt i tabellen kan indeholde flere produktioner. Hvad gør man sa?

Svaret er, at i dette tilfælde kan vi ikke anvende LL(1)-parseren til denne grammatik.Faktisk har vi følgende definition:

Definition 39En grammatik er LL(1), hvis hvert felt i parse-tabellen indeholder højst en produktion.

En anden made at formulere dette pa er følgende:

Sætning 40En grammatik er LL(1), hvis og kun følgende to betingelser gælder:

a) For enhver non-terminal A med samtlige produktioner A → α1|α2| · · · |αn gælderfor alle i �= j, at first(αi) ∩ first(αj) = ∅.

b) For enhver nullable non-terminal A gælder, at first(A) ∩ follow(A) = ∅

For at se, hvorfor betingelsen b) skal holde, kan vi bemærke, at hvis A har samtlige pro-duktioner A→ α1|α2| · · · |αn, sa er first(A) = first(α1) ∪ first(α2) ∪ · · · first(αn).

Eksempel 41Et eksempel pa en grammatik, der ikke er LL(1), er følgende:

S → aSA|ε A→ abS|cGroft sagt er dette ikke en LL(1)-grammatik, da vi i situationen med S øverst pa stak-ken og a i inputtet ikke kan vælge, om vi skal anvende produktionen S → aSA ellerproduktionerne S → ε og dernæst A→ abS.

De gustne detaljer er:

nullable first followS true a a, c, $A false c a, c, $

a b c $S S → aSA og S → ε S → ε S → εA A→ abS A→ ε

Bemærk konflikten i cellen M [S, a].

45

Page 46: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Et andet eksempel er:

Eksempel 42Grammatikken

S → aA|aB A→ a B → b

er ikke LL(1). Problemet er, at i situationen med S øverst pa stakken og lookahead a ervi i en konflikt — skal vi vælge produktionen S → aA eller S → aB?

I dette tilfælde kan vi nemt løse problemet ved at omskrive grammatikken — vi laveren sakaldt venstre-faktorisering :

Hver gang der er en non-terminal med to (eller flere) produktioner med ens start, saindfører vi en ny non-terminal, faktoriserer denne start ud, og lader den ny non-terminalproducere resterne.

F.eks. vil reglerne S → aA|aB blive til S → aS ′ og S ′ → A|B. Hermed udsætter videt tidspunkt, hvor vi skal vælge, til efter vi har læst og behandlet a’et.

Et andet alternativ til venstre-faktorisering er at anvende en LL(2)-grammatik. Her har viet lookahead pa 2, og i eksemplet løser vi snildt problemet: Hvis symbolet pa lookahead-plads2 er et a, anvender vi S → aA, og er det et b, anvendes S → aB.

LL(k)-grammatikker anvendes ikke sa meget i praksis: Idet vi har et større lookahead, sabliver elementerne i first- og follow-mængderne k-tupler af terminaler. Har vi n terminaleri grammatikken, sa findes der nk mulige k-tupler, og for realistiske værdier af k og n bliverdette tal astronomisk. Parse-tabellen bliver ogsa meget stor. Endelig løser LL(k)-parseren ikkevores problem med venstre-rekursion...

Opgaver

Opgave 4.1 Gennemfør en parsing af strengene (x+ x) ∗ x, x+ og ∗+ x i grammatikken fraeksempel 31.

(Ja, de to sidste strenge skulle gerne ende i en error).

Opgave 4.2 Beregn first- og follow-mængderne for grammatikken i eksempel 42 og konstruerparse-tabellen.

Vis dernæst, at den venstre-faktoriserede grammatik i eksempel 42 er LL(1), men at denoprindelige grammatik ikke er det.

Opgave 4.3 Hvilke sprog frembringer nedenstaende grammatikker? Bestem dernæst first- ogfollow-mængderne, og opskriv parse-tabellen.

a) S → ASb|C A → a C → cC|εb) S → aSb|εc) S → aSB|C B → b C → c

Opgave 4.4 Find en LL(1) grammatik for hver af nedenstaende sprog:

a) {a, ba, bba}b) {anb | n > 0}

46

Page 47: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

c) {ambncm+n | m,n ≥ 0}

Opgave 4.5 Betragt følgende grammatik, som genererer lister a la Lisp:

lexp → atom|listatom → number |identifier

list → ( lexp-seq )lexp-seq → lexp-seq lexp|lexp

a) Fjern venstre-rekursionenb) Beregn nullable, first og follow for den transformerede grammatikc) Opskriv parsetabellend) Vis i detaljer, hvorledes strengen (a(b(2))(c)) parses

Opgave 4.6 Betragt følgende grammatik, som genererer lister a la Lisp:

lexp → atom|listatom → number |identifier

list → ( lexp-seq )lexp-seq → lexp, lexp-seq |lexp

(Og nej, det er ikke den samme grammatik som i opgave 4.5)

a) Venstre-faktoriser grammatikken (den sidste produktion)b) Beregn nullable, first og follow for den transformerede grammatikc) Opskriv parsetabellend) Vis i detaljer, hvorledes strengen (a, (b, (2)), (c)) parses

Opgave 4.7 Betragt følgende grammatik for simplificerede Java deklarationer:

decl → type var-listtype → int|floatvar-list → identifier, var-list |identifier

a) Venstre-faktoriser grammatikkenb) Beregn nullable, first og follow for den transformerede grammatikc) Opskriv parsetabellend) Vis i detaljer, hvorledes strengen int x,y parses

Opgave 4.8 Betragt følgende grammatik for simplificerede Java statements:

statement → assign-stmt |call-stmt |otherassign-stmt → identifier = exp

call-stmt → other ( exp-list )

Venstre-faktoriser grammatikken.

47

Page 48: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

5 LR-parsing

Et ganske anvendeligt alternativ til LL-parseren er de sakaldte LR-parsere. Ideen er her ikkeat forudsige, hvilke produktion, vi vil anvende, men i stedet vente og tage beslutningen, naralle symbolerne pa produktionens højreside er genkendte.

Dette kunne se ud til at betyde, at vi skal kunne kigge ned i parserens stak, men da detjo pa en made er snyd — i en stak kan man kun se det øverste symbol — og endvidere retineffektivt, da vi skal undersøge mange kombinationer pa stakken hele tiden, bruger vi i stedeten endelig automat til at huske pa, hvad der er nede i stakken.

Som vi skal se, findes LR-parseren i flere varianter, men selve den fundamentale parse-algoritme er den samme — forskellen ligger i konstruktionen af den endelig automat og dermedi parse-tabellen.

Navnet ’LR’ skyldes i øvrigt, at man læser inputstrengen fra venstre (Left), og man opnaren højre-derivation af strengen (Right). Faktisk er det en fordel med en højre-derivation —venstre-rekursion er intet problem, og idet parseren venter med at ”fyre”en produktion af,indtil højresiden er genkendt, sa kan højre-rekursion heller ikke irritere.

En anden fordel ved LR-parseren er, at det er uhyre nemt at konstruere parse-træet —idet vi far en højre-derivation (hvor produktionerne faktisk kommer i omvendt rækkefølge),opbygges parse-træet faktisk fra bladene og imod roden. Vi siger ogsa, at LR-parseren er enbottom-up-parser.

5.1 Bottom-up parsing

Vi starter med at se pa den fundamentale algoritme for LR-parseren, Et andet navn for dennetype parsere er shift-reduce-parsere, idet algoritmen i hvert eneste trin har to muligheder: Shift,hvorved et inputsymbol flyttes over pa parserens stak, og reduce, hvorved en delstreng paparserens stak reduceres baglæns til en non-terminal vha. en af grammatikkens produktioner.

Dette lyder lidt obskurt, sa lad os give et eksempel:

Eksempel 43Betragt grammatikken

S ′ → S$ S → (S)S|εsom genererer alle strenge af matchende parenteser. (Denne grammatik er, som allegrammatikker, der kan parses af LR-parsere, forstærket, dvs. der er tilføjet en extraproduktion og et extra startsymbol S ′. Dette er nødvendigt af tekniske arsager.)

En buttom-up-parsing af strengen ’()$’ i denne grammatik kunne se ud som følger:

stak input handling$ ()$ shift$( )$ reduce S → ε$(S )$ shift$(S) $ reduce S → ε$(S)S $ reduce S → (S)S$S $ reduce S ′ → S$$S ′ $ strengen accepteres

48

Page 49: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Vi vil endnu ikke forklare, hvordan parseren fandt ud af, om der skulle shiftes ellerreduces, og i sa fald med hvilken produktion.

Bemærk, at tager vi de produktioner, parsingen spyttede ud, og anvender i omvendtrækkefølge, sa far vi netop en højre-derivation af inputstrengen:

S ′ ⇒ S$ ⇒ (S)S$ ⇒ (S)$ ⇒ ()$

Generelt ser vi, at læser vi alle symbolerne pa stakken pa et givet tidspunkt i omvendtrækkefølge, sa har vi den første del af en sætning i højre-derivationen (en sakaldt højre-sætning).

For at kunne bestemme, om der skal shiftes eller reduces er det nødvendigt for parseren atkunne kigge pa alle symbolerne i stakken, ikke kun det øverste. Dette er naturligvis snyd, daen stak netop er karakteriseret ved, at man kun kan se det øverste element, men buttom-upparseren løser problemet pa en meget elegant made — da parseren selv opbygger stakken, sakan den løbende gemme information om de forskellige symboler i stakken. Det viser sig, at enendelig automat virker fortrinligt til dette.

LR-parser

Stak

Input

Output

a + b #

(X,s)

(Y,t)

(Z,u)

(#,0) action goto

Figur 19

Opbygningen af en LR-parser er vist pa figur 19. Det ses, at parseren bestar af et program,en stak, en parse-tabel opdelt i to dele, kaldet action- og goto-tabellen, og endelig inputstren-gen. Pa stakken pushes og poppes der par (X, s), hvor X er et symbol fra grammatikken ogs er en tilstand i den føromtalte endelige automat. (I praksis kan man nøjes med at placeretilstandene pa stakken). Action-tabellen indeholder et felt for hver tilstand og hver terminal,mens goto-tabellen indeholder et felt for hver tilstand og hver non-terminal.

I begyndelsen er vi i starttilstanden 0 af den endelige automat, og pa stakken ligger parret(S ′, 0), hvor S ′ er startsymbolet.

49

Page 50: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Vi gennemløber nu nedenstaende løkke:Antag, at stakken har udseendet (X0, s0)(X1, s1) . . . (Xm, sm) med parret (Xm, sm) øverst.

Den endelige automat er derfor i tilstand sm. Antag endvidere, at den resterende del afinputstrengen er aiai+1 . . . an.

Vi læser nu ai uden at fjerne den fra inputstrengen og slar op i action-tabellen underaction[ai,sm]. Der kan nu ske en af følgende muligheder

a) Hvis action[ai,sm] = ’shift s’, sa udfører parseren en shift-handling, dvs. den sletter ai

fra inputstrengen, pusher (ai, s) pa stakken og gar til tilstand s.

b) Hvis action[ai,sm] = ’reduce(A→ α)’, sa udfører parseren en reduce-handling, dvs. denpopper r par fra stakken, hvor r er længden af strengen α, saledes at parret(X, sm−r)bliver synligt i stakken, og bevæger sig over i tilstanden s = goto[A,sm−r]. Inputstrengenberøres ikke. Man vil her kunne konstatere, at de symboler, der poppes af stakken, netoptilsammen udgør α — reduce-produktionens højreside.

c) Hvis action[ai,sm] = ’accept’, sa accepteres strengen, og vi bryder ud af løkken

d) Hvis action[ai,sm] = ’error’, sa er der en fejl, og parsingen stopper

Vi vil i det følgende konkretisere denne algoritme i fire tilfælde: LR(0)-parsere, SLR(1)-parsere, LR(1)-parsere og LALR(1)-parsere. Den eneste forskel pa disse 4 algoritmer er, hvor-ledes parse-tabellen konstrueres.

5.2 LR(0)-parsing

Vi vil nu illustrere, hvor denne føromtalte endelige automat kommer ind i spillet, og dernæstvil vi beskrive, hvorledes en LR(0)-parser fungerer.

Tallet 0 dukker op, fordi denne type parser ikke kigger fremad i inputstrengen, men ude-lukkende vælger den næste handling ud fra sit kendskab til, hvad der befinder sig pa parse-stakken.

Dette gør LR(0)-parserens action-tabel lidt primitiv — som vi skal se, indeholder den entenen shift- eller en reduce-handling for hver eneste tilstand.

I praksis anvendes LR(0)-parseren ikke; men da den er relativt simpel og nødvendig forforstaelsen af LR(1)-parseren, gennemgas den her.

Lad os starte med at konstruere den endelig automat, der ligger til grund for LR(0)-parsingen:

Tilstandene er sakaldte poster: En post (eng: item) er en produktion i grammatikken meden udvalgt position pa højresiden.

Har vi f.eks. produktionen S → (S)S, sa far vi posterne:

S → ·(S)S S → (·S)S S → (S·)S S → (S) · S S → (S)S·hvor vi angiver den udvalgte position med en prik.

Posten S → (S·)S angiver, at vi forventer senere at kunne reducere vha. produktionenS → (S)S, og at ( og S allerede ligger pa stakken.

Specielt angiver en post med prikken til sidst, f.eks. S → (S)S·, at vi har alle symbolernepa højresiden pa stakken og at vi derfor nu skal reducere med den relevante produktion. Ensadan post kaldes en fuldstændig post.

50

Page 51: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Omvendt angiver en post med prikken i starten af højresiden, f.eks. S → ·(S)S, at vi endnuikke har set nogle som helst af de symboler, vi skal bruge for at kunne reducere. En sadanpost kaldes en initial post.

Lidt speciel er en ε-produktion, f.eks. S → ε . Den giver kun anledning til en post, nemligS → ·, men til gengæld er denne post bade initial og fuldstændig.

Vi kan nu konstruere vores endelige automat. Hver eneste tilstand er som sagt en post,men da vi kan komme ud for, at der er flere mulige produktioner, vi kan reducere med, altefter hvad der er pa parserens stak, er vi nødt til at kunne være i flere tilstande samtidigt. Vihar altsa en non-deterministisk endelig automat.

Overgangene i automaten bestemmes ud fra, hvilket symbol X der placeres pa stakkennæste gang. Antag, at vi har produktionen A→ αXβ og betragt posten A→ α ·Xβ. Vi vilhave posten A→ αX · β efter at have placeret X pa stakken, hvilket altsa giver overgangen

X : A→ α ·Xβ til A→ αX · βHvis X endvidere er en non-terminal, sa skal vi have reduceret en hel række terminaler til Xfor at kunne komme videre, og derved fa placeret X pa stakken, før vi kan lave overgangentil A→ αX ·β, men dette kan kun forega ved reduktion af en eller anden produktion X → γ.Dette betyder, at vi nu skal være parate til at reducere vha. samtlige X-produktion. Vi faraltsa ε-overgange fra tilstanden A → α · Xβ til alle initiale poster X → ·γ, og dette gælderfor alle X-produktionerne.

Hvad er startilstanden for automaten? Dette er lidt problematisk, fordi hver eneste initialepost for startsymbolet S kunne være en starttilstand, og hvordan skal vi sa kunne vælge?

Løsningen er at forstærke grammatikken, dvs. indføre et nyt startsymbol S ′ med den enligeproduktion S ′ → S$. Vi far da starttilstanden S ′ → ·S ′$ og ε-overgange til alle tilstandeS → ·σ.

Bemærk, at idet vi aldrig læser forbi $-tegnet, sa vil S ′ → S$· ikke være en post, deroptræder i praksis.

Hvad er sa sluttilstanden? Tjah, vi bruger jo den endelige automat til at huske pa, hvadder ligger pa stakken, ikke til at acceptere strenge, sa der er lidt meningsløst at tale omsluttilstande. I stedet har vi nogle tilstande, nemlig svarende til de fuldstændige poster, somangiver, at her foregar er reduktion, men selv efter reduktionen vil vi stadig være i en tilstandi den endelige automat.

Eksempel 44Betragt den forstærkede grammatik:

0: S ′ → S$

1: S → (S)S

2: S → ε

(Det viser sig at være smart at opskrive produktionerne en for en, og nummereredem)

Lad os konstruere NFA’en svarende hertil. De mulige poster er:

S ′ → ·S$ S ′ → S · $ S → ·(S)S S → (·S)SS → (S·)S S → (S) · S S → (S)S · S → ·

Med alle overgangene fas NFA’en pa figur 20.

51

Page 52: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

S' S�. $ S' S� .$

S S S�.( )

S S S�( ). S S S�( ). S S S�( ).

S S S�( ) .S�.

S

(

)

��

� �

S

S�

Figur 20

Nu er NFA’er jo generelt ubehagelige at arbejde med, sa vi kan transformere den til en DFApa sædvanlig made. Men da dette er en omstændelig proces, vælger vi i stedet at konstruereDFA’en direkte:

Ideen er at starte med posten S → ·S$, lukke denne tilstand, finde en række nye tilstande udfra denne, lukke disse tilstande, etc., indtil der ikke kommer flere nye tilstande og overgange.

Definition 45Lad T være en mængde af poster. Lukningen af T beregnes saledes:

For hver post A → α · Xβ, hvor X er en non-terminal, tilføjes alle initiale posteraf formen X → ·γ. Denne proces fortsættes iterativt, indtil der ikke kan tilføjes flereposter.

Lad os illustrere fremgangsmaden med et eksempel:

Eksempel 46Vi konstruerer DFA’en for grammatikken fra eksempel 44 direkte.

Starttilstanden indeholder posten S ′ → ·S$. Da vi har en prik foran en non-terminalS, lukker vi denne tilstand ved at tilføje posterne S → ·(S)S og → ·ε. Da vi ikke fik flereposter med prikken foran non-terminaler, er denne tilstand altsa lukket. Vi nummerertilstandene, og tilstand 0 er altsa givet ved:

{S ′ → ·S$ , S → ·(S)S , S → ·}Fra tilstand 0 har vi en S- og en (-overgang, idet der findes prikker foran disse symboler.

S-overgangen giver tilstand 1, som umiddelbart indeholder posten S ′ → S · $. Dader her ingen prikker er foran non-terminaler, er denne tilstand lukket, og da vi ikkebetragter $-overgange, sa kan vi ikke gøre mere ved denne tilstand.

52

Page 53: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Tilstand 2 indeholder umiddelbart posten S → (·S)S. Da vi har prikken foran non-terminalen S, lukkes tilstanden ved at tilføje posterne S → ·(S)S og S → ·ε. Tilstand 2bliver altsa

{S → (·S)S , S → ·(S)S , S → ·}Fra tilstand 2 har vi en (-overgang og en S-overgang. Det ses, at (-overgangen fører

tilbage til lukningen af S → (·S)S, dvs. til tilstand 2 selv.S-overgangen fører til en post S → (S·)S. Da der ingen prik er foran non-terminaler,

er denne tilstand lukket. Vi kalder tilstanden 3.Fra tilstand 3 er der en )-overgang til S → (S) · S. Denne nye tilstand 4 lukkes til

{S → (S) · S , S → ·(S)S , S → ·}Fra tilstand 4 er der en (-overgang til tilstand 2 og en S-overgang til en ny tilstand.

Denne nye tilstand 5 indeholder posten S → (S)S· og er lukket.Alt i alt far vi DFA’en pa figur 21.

S' S�. $

S' S� .$S S S�.( )

S S S�( ).

S S S�( ).

S S S�( ).

S S S�( ) .

S�.

S

(

)S

S

S S S�.( )

S�.

(

(

S S S�.( )

S�.

0

1

2

3

4

5

Figur 21

Vi kan nu endelig beskrive LR(0)-parsetabellen:

Definition 47LR(0)-parsetabellen konstrueres pa følgende vis:

53

Page 54: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

a) Lav en tabel, indekseret ved alle tilstande i DFA’en og ved alle symboler i gram-matikken. Man plejer at skive alle terminaler først, og dernæst alle non-terminaler.Herved fas automatisk opdelingen i action- og goto-tabellen.

b) For hver eneste X-overgang fra tilstand s til tilstand t tilføjes følgende i cellen(s,X): Hvis X er en terminal (eller $), tilføjes ”shift t”. Hvis X er en non-terminal,tilføjes ”goto t”.

c) For hver eneste tilstand s, der indeholder en fuldstændig post A → α·, tilføjes”reduce A→ α”til alle celler (s, x) for alle terminaler x.

d) I cellen (1,x) skrives ”accept-– dette er en forkortelse for ”reduce S ′ → S$”.

e) I alle tomme celler skrives error.

I praksis anvender vi følgende forkortelser: ”shift t”erstattes med ”st”, ”goto t”erstattesmed ”gt”, og ”reduce A→ α”forkortes med ”rn”, hvor t er nummeret pa en tilstand, og n ernummeret pa produktionen A→ α. ”accept”forkortes med ”acc”.

Endvidere vil vi aldrig have nogle S ′-overgange, sa start-non-terminalen S ′ udelades ofteaf goto-tabellen.

Eksempel 48Vi kan nu endelig opskrive LR(0)-parsetabellen for grammatikken fra 44 og med LR(0)-DFA’en pa figur 21.

Den tomme LR(0)-parsetabel er:

( ) $ S012345

Indsætter vi alle shift- og goto-handlingerne, fas:

( ) $ S0 s2 g112 s2 g33 s44 s2 g55

Endelig kommer alle reduce-handlingerne (herunder ”accept”):

( ) $ S0 s2/r2 r2 r2 g11 acc acc acc2 s2/r2 r2 r2 g33 s44 s2/r2 r2 r2 g55 r1 r1 r1

54

Page 55: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Som det ses, har vi visse problemer i cellerne (0,’(’), (2,’(’) og (4,’(’) — vi ved ikke,om vi skal shifte eller reduce.

Disse shift-reduce-konflikter betyder faktisk, at grammatikken ikke er LR(0), ogLR(0)-parsings-algoritmen virker altsa ikke i dette tilfælde . . .

Definition 49En grammatik er LR(0), hvis der ikke optræder shift-reduce eller reduce-reduce-konflikteri LR(0)-parsetabellen.

Reduce-reduce-konflikter kan optræde, hvis der er to fuldstændige poster i samme tilstand.Der kan aldrig optræde shift-shift-konflikter.

I almindelighed er det kun meget simple grammatikker, der er LR(0). Lad os give eteksempel:

S' S�. $S S�.( )

S a� .

S S�( ).

S S�( ).

S a�.

)

(

0

2

34

5

a

a

S

S' S.� $

1

S

S S��. )S S�.( )

S a�.(

Figur 22

Eksempel 50Betragt grammatikken:

0: S ′ → S$

0: S → (S)

0: S → a

som frembringer strenge af formen ((. . . ((a)) . . . ) med lige mange venstre-og højre-parenteser.

DFA’en ses pa figur 22, og parsetabellen bliver

55

Page 56: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

( a ) $ S0 s3 s2 g11 acc acc acc acc2 r2 s2 r2 r23 s3 s2 g44 s55 r1 r1 r1 r1

Lad os parse strengen ((a))$: (Indexene pa elementerne i stakken henviser til dentilstanden i DFA’en).

stak input handling$0 ((a))$ shift3$0(3 (a))$ shift3$0(3(3 a))$ shift2$0(3(3a2 ))$ reduce S → a$0(3(3S4 ))$ shift5$0(3(3S4)5 )$ reduce S → (S)$0(3S4 $ shift5$0(3S4)5 $ reduce S → (S)$0S1 $ accept

5.3 SLR(1)-parsing

Vi skal nu se pa SLR(1)-parsing (’S’ star for ’simple’). Dette er en relativ enkel generaliseringaf LR(0)-parseren:

I SLR(1)-algoritmen tillader vi parseren at læse det næste symbol i inputstrengen, før denbeslutter sig for at shifte eller reduce. Herved kan vi løse en hel del shift-reduce-konflikter(men desværre ikke reduce-reduce-konflikter).

Men hvornar er det rimeligt at foretage operationen ”reduce ”? Ja, det er det kun, hvislookahead-terminalen faktisk ligger i follow(X).

Vi far derfor konstruktionen af SLR(1)-parsetabellen (DFA’en er den samme som forLR(0)):

Definition 51SLR(1)-parsetabellen konstrueres pa følgende vis:

a) Lav en tabel, indekseret ved alle tilstande i DFA’en og ved alle symboler i gram-matikken. Man plejer at skive alle terminaler først, og dernæst alle non-terminaler.Herved fas automatisk opdelingen i action- og goto-tabellen.

b) For hver eneste X-overgang fra tilstand s til tilstand t tilføjes følgende i cellen(s,X): Hvis X er en terminal (eller $), tilføjes ”shift t”. Hvis X er en non-terminal,tilføjes ”goto t”.

c) For hver eneste tilstand s, der indeholder en fuldstændig post A → α·, tilføjes”reduce A→ α·”til alle celler (s, x) for alle terminaler x ∈ follow(A)

d) I cellen (1, $) skrives ”accept” — dette er en forkortelse for ”reduce S → S ′”

56

Page 57: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

e) I alle tomme celler skrives error

Definition 52En grammatik er SLR(1), hvis der ikke er konflikter i SLR(1)-parsetabellen, dvs. nar

a) For enhver post A→ α · xβ, hvor x er en terminal, er der ingen fuldstændig postX → γ· i samme tilstand med x ∈ follow(X)

b) For to fuldstændige poster X → γ· og Y → δ· i samme tilstand er follow(X) ∩follow(Y ) = ∅

Forklaringen herpa er, at overholdes regel a) ovenfor ikke, sa risikerer vi en shift-reduce-konflikt: Skal vi shifte over til tilstanden med posten A → αx · β, eller skal vi reducere efterproduktionen X → γ? Tilsvarende kommer men i poster, hvor regel b) ikke gælder, i tvivlom, hvorvidt man skal recudere efter den ene eller den anden regel.

Eksempel 53Grammatikken fra eksempel 44 er i virkeligheden SLR(1). Vi har nemlig følgende be-regninger:

nullable first followS ′ false ( $S true ( ),$

og SLR(1)-parsetabellen bliver

( ) $ S0 s2 r2 r2 g11 acc2 s2 r2 r2 g33 s44 s2 r2 r2 g55 r1 r1

Ved at anvende follow-reglen undgik vi behændigt shift-reduce-konflikterne i tilstan-dene 1, 2 og 4 for terminalen (.

5.4 LR(1)-parsing

Den mest generelle, men ogsa mest besværlige bottom-up-parsing er LR(1)-parsing. Fordelenved denne metode er, at alle deterministiske, kontekst-frie sprog kan LR(1)-parses (omendman maske skal omskrive grammatikken) — dette betyder i praksis, at alle forekommendeprogrammeringssprog kan parses af en LR(1)-parser. Desværre er konstruktionen af DFA’enog parse-tabellen er en del mere kompliceret end for SLR(1)-parseren, og bade LR(1)-DFA’enog LR(1)-parsetabellen er væsentligt større end ved SLR(1)-parseren.

DFA’en konstrueres stort set pa samme made som LR(0)-parseren, men posterne er ikkebare en produktion med en udvalgt position: En LR(1)-post er et ordnet par bestaende af enLR(0)-post samt en terminal. Vi anvender følgende notation:

[A→ α ·Xβ, x]

57

Page 58: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

for en LR(1)-post. Selve LR(0)-posten kaldes ofte kernen, mens den ekstra terminal x betegnessom et lookahead.

Betydningen af LR(1)-posten [A→ α ·Xβ, x] er, at vi i en tilstand med denne post alleredehar faet placeret α pa stakken, og vi er villige til at genkende et X, men kun hvis dette Xefterfølges af en streng, der kan deriveres fra βx.

Fordelen ved at udvide posterne med et symbol er, at vi nu kan tage hensyn til det næstesymbol i inputstrengen direkte i den endelige automat og ikke først i parsetabellen. Vi opnarderved en mere forfinet DFA, som bedre er i stand til at skelne mellem forskellige situationer.

Konstruktionen af LR(1)-DFA’en og LR(1)-parsetabellen foregar stort set pa samme madesom for LR(0)-parseren: DFA’en konstrueres ved at tage en start-post, lukke denne, konstruerenye tilstande ud fra forskellige overgange, aflukke disse, etc.

Eneste forskel ligger faktisk i, hvordan vi finder lukningen af en tilstand.

Definition 54Lad T være en mængde af LR(1)-poster. Lukningen af T beregnes saledes:

For hver post [A→ α ·Xβ, x], hvor X er en non-terminal, tilføjes alle initiale posteraf formen [X → ·γ, y] for alle y ∈ first(βx). Denne proces fortsættes iterativt, indtil derikke kan tilføjes flere poster.

Ideen er, at for at kunne genkende produktionen X → γ med det rigtige lookahead, sa skaldette lookahead komme fra det, der kan følge efter X, nemlig βx.

Som start-post til konstruktionen af automaten har vi LR(1)-posten [S ′ → S$, ?], hvorlookahead ’?’ betyder, at vi her er ligeglade med lookahead — vi kan jo alligevel aldrig kiggeforbi slutsymbolet $.

Eksempel 55Lad os konstruere LR(1)-DFA’en for grammatikken 50

0: S ′ → S$

0: S → (S)

0: S → a

Vi starter tilstand 0 med posten [S ′ → S$, ?]. Da vi har en prik foran non-terminalenS, tilføjer vi to nye poster, nemlig [S → ·(S), $] og [S → ·a, $].

Fra tilstand 0 har vi en S-overgang til tilstand 1 bestaende af posten [S ′ → S · $, ?].Vi har ogsa en (-overgang til tilstand 2. Denne bestar af posten [S → (·S), $], og ved

lukningen heraf far vi tilføjet [S → ·(S), )] og [S → ·a, )] — bemærk, at first(′)$′) = {)}.Endelig har vi en a-overgang til posten [S → a·, $]. Denne er lukket.Og saledes fortsætter vi videre fra tilstand 2....Resultatet bliver som pa figur 23.Bemærk forskellen pa f.eks. tilstandene 4 og 8. I tilstand 4 er vi helt sikkert ved

at genkende den sidste højre-parentes, mens vi i tilstand 8 har gang i en indre højre-parentes. LR(1)-DFA’en er altsa i stand til at skelne mellem flere situationer end LR(0)-DFA’en.

Opskrivningen af LR(1)-parsetabellen foregar nu ved:

58

Page 59: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

[ $,?]S' S�. [ ,?]S' S� .$[ ( ) $]S S ,�.

[ ( ),$]S S.�

[ ,)]S a� .

[ ,$]S a�.

a(

0

1

2

4

6

S

[ $S' a� ., ]

3

S

(5

[ ),$]S S��.

[ ( ) )]S S ,�.

[ ,)]S a�.

[ ,)]S S��� �

[ ( ) )]S S ,�.

[ ,)]S a�.

(

a

a

[ ( ).,$]S S�7

)

[ ( ),)]S S.�8

S [ ( ).,)]S S�9

)

Figur 23

59

Page 60: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Definition 56LR(1)-parsetabellen konstrueres pa følgende vis:

a) Lav en tabel, indekseret ved alle tilstande i DFA’en og ved alle symboler i gram-matikken

b) For hver eneste X-overgang fra tilstand s til tilstand t tilføjes følgende i cellen(s,X): Hvis X er en terminal (eller $), tilføjes ”shift t”. Hvis X er en non-terminal,tilføjes ”goto t”

c) For hver eneste tilstand s, der indeholder en fuldstændig post [X → γ·, x], tilføjes”reduce X → γ”til cellen (s, x)

d) I cellen (1,$) skrives ”accept”

e) I alle tomme celler skrives error

Eksempel 57Lad opskrive LR(1)-parsetabellen for grammatikken fra eksemplet fra før. Med alle shiftog goto’er, far vi

( a ) $ S0 s2 s3 g112 s5 s6 g434 s75 s5 s6 g8678 s99

Med reduce-operationerne, fas

( a ) $ S0 s2 s3 g11 acc2 s5 s6 g43 r24 s75 s5 s6 g86 r27 r18 s99 r2

Som før nævnt kan LR(1)-parseren parse alle i praksis forekommende grammatikker. Uhel-digvis har LR(1)-parseren den ulempe, at bade DFA’en og parsetabellen let gar hen og bliverenorme, med store udførselstider bade i konstruktion og især i udførsel til følge.

Vi skal derfor se pa det sidste alternativ, nemlig LALR(1)-parseren.

60

Page 61: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

5.5 LALR(1)-parsing

En noget simplere parsingsmetode end LR(1) er LALR(1) — ’LA’ star for ’lookahead’. Ideenher er, at mange af tilstandene i LR(1)-algoritmens DFA kan slas sammen:

Ser vi f.eks. pa DFA’en fra figur 23, ser vi, at den eneste forskel pa tilstandene 2 og 4 erlookahead-symbolerne. Tilsvarende er der ikke den store forskel pa tilstand 3 og 6, 4 og 8 ogpa tilstand 7 og 9.

Ideen bag LALR(1)-parseren kommer fra følgende konstatering:Har vi to LR(1)-tilstande med de samme kerneposter, men forskellige lookaheads, sa vil de

to tilstande have overgange til tilstande med tilsvarende ens kerneposter.Hermed menes, at hvis s og s′ har ens kerneposter, og der er en X-overgang fra s til t, sa

vil der ogsa være en X-overgang til en tilstand t′, og t og t′ vil have ens kerneposter.Det giver derfor mening at sla tilstande med ens kerneposter sammen.

[ $,?]S' S�.[ $,?]S' S� .

[ ( ) $]S S ,�.

[ ( ),$)]S S.�

[ ,$]S a�.a

(

0

1

2

4

S

[ $S' a� ., )]

3

S

[ ),$)]S S��.

[ ( ) )]S S ,�.

[ ,)]S a�.(

a

[ ( ).,$)]S S�5

)

Figur 24

Prøver vi dette pa DFA’en fra figur 23, fas LALR(1)-DFA’en pa figur 24, hvor vi anvenderforkortelsen [A→ α ·Xβ, xy] for de to poster [A→ α ·Xβ, x] og [A→ α ·Xβ, y].

Den tilhørende LALR(1)-parsetabel bliver:

( a ) $ S0 s2 s3 g11 acc2 s5 s3 g43 r2 r24 s55 r1 r1

Det ses, at LALR(1)-DFA’en pa figur 24 ligner meget LR(0)-DFA’en fra figur 21— deneneste forskel ligger i lookaheads’ene. Dette kan vi faktisk udnytte til at konstruere LALR(1)-

61

Page 62: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

DFA’en uden at skulle gennem den trælsomme LR(1)-DFA. Metoden hedder lookahead pro-pagation.

Vi konstruerer først LR(0)-DFA’en pa sædvanlig vis. Dernæst tilføjer vi lookaheads efterfølgende regler, som minder meget om LR(1)-lukningsprocessen:

Definition 58a) Konstruer LR(0)-DFA’en

b) Initier processen ved at tilføje lookahead ? (betydende et vilkarligt tegn) til postenS ′ → ·S$

c) For hver post [A→ α·Xβ, x] med lookahead x tilføjes samtlige symboler i first(βx)til alle initiale poster af formen X → ·γ i samme tilstand

d) For hver X-overgang fra en tilstand med posten A → α ·Xβ til en tilstand medposten A→ αX · β forplantes ethvert lookahead x fra A→ α ·Xβ til A→ αX · β

e) Denne proces fortsættes, indtil situationen er stabil

f) Selve parsetabellen laves som ved LR(1)-parseren.

[ $,?]S' S�.

[ $,?]S' S� .[ ( ) ,$]S S S�.

[ ( ) ,$)]S S S� .

[ ( ) ,$)]S S S� .

[ ( ) ,$)]S S S� .

[ ( ) $)]S S S� .

[ ,$]S�.

S

(

)S

S

[ ( ) ,$)]S S S�.

[ ,(]S�.

(

(

[ ( ) ,(]S S S�.

[ ,$)]S�.

0

1

2

3

4

5

Figur 25

Eksempel 59

62

Page 63: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Lad os konstruere LALR(1)-DFA’en for grammatikken fra eksempel 44. Vi har alleredeLR(0)-DFA’en pa figur 21, sa vi skal bare tilføje lookaheads.

Vi starter med at tilføje lookaheadet ? til i tilstand 0.Regel c) fortæller os nu, at $ skal tilføjes posterne S → ·(S)S og S → · i tilstand 0.

Da der ikke er nogen overgange til tilstand 0, er vi færdige med denne tilstand.Regel d) fortæller, at ? skal tilføjes S ′ → S · $ i tilstand 1.Regel d) fortæller ligeledes, at $ skal tilføjes posten S → (·S)S i tilstand 2. Ifølge

regel c) tilføjes ) til S → ·(S)S og S → · i samme tilstand.Der er en )-overgang fra tilstand 2 til sig selv, og regel c) giver, at ) skal tilføjes til

posten S → (·S)S i tilstand 2.Regel d) tilføjer $ og ) til posten S → (S·)S i tilstand 3.Igen regel d) — fra tilstand 3 forplanter $ og ) sig til posten S → (S) · S i tilstand 4.

Regel c) sender $ og ) videre til de to andre poster i tilstand 4 ogsa.Regel c) sender ligeledes ) og $ videre til posten i tilstand 5.Endelig ser vi pa (-overgangen fra tilstand 4 til tilstand 2. Regel c) fortæller ogsa, at

$ og ) skal tilføjes S → (·S)S i tilstand 2, men da disse allerede er der, sa sker der intet.Det ses, at situationen er stabiliseret.Resultatet er vist pa figur 25.LALR(1)-parsetabellen bliver

( ) $ S0 s2 r2 r2 g11 acc2 s2 r2 r2 g33 s44 s2 r2 r2 g55 r1 r1

hvilket ikke adskiller sig synderligt fra den tilsvarende SLR(1)-parsetabel.

LALR(1) er den parser, der anvendes mest i praksis. Bl.a. anvender parsergeneratorenCUP, som vi skal se pa i næste kapitel, LALR(1)-parsing.

5.6 Parsing af tvetydige grammatikker

I visse tilfælde kan man fa en LR-parser til at parse tvetydige grammatikker pa fornuftig vis.Vi giver et eksempel:

Eksempel 60Betragt nedenstaende variant af if-else-grammatikken fra kapitel 3:

0: S ′ → S$

0: S → if exp S

0: S → if exp S else S

0: S → other

63

Page 64: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

S' S�. $ S' S� .$S S�.if exp

S S S�.if elseexp

S

0

2

S�.other

S�other.

1

other

S S�if.expS S S�if. elseexp

3

S S�.if expS S S�if . elseexp

4

S S�.if expS S S�.if elseexp

6

S S�if exp .S S S�if elseexp .

5

S S S�if else .exp

7

S S S�.if elseexp

S S�if .exp

S S S�if else.exp

S

S

exp

if

if

if

else

Figur 26

(for simpelheds skyld udelader vi then.)Denne grammatik er tvetydig — har vi to indlejrede if-sætninger med en efterfølgende

else, sa er det uklart, til hvilken if denne else tilhører. I det fleste programmerings-sprog er konventionen, at else’n hører til den sidste if, men denne regel er svær atinkorporere i grammatikken.

Lad os konstruere LR(0)-DFA’en (figur 26 og SLR(1)-parsetabellen:

64

Page 65: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

if exp else other $ S0 s3 s2 g11 acc2 r3 r33 s44 s3 g55 s6 / r1 r16 s3 g77 r2 r2

Som det ses, udmønter tvetydigheden sig i en shift-reduce-konflikt i tilstand 5: Skalvi shifte en else eller reduce efter produktionen S → if exp S ?

Det viser sig, at hvis vi altid shifter, sa løser vi else-problematikken fra før: Ved atshifte fremfor at reduce far vi altid en else til at høre sammen med den sidste if.

Denne situation optræder en del steder i grammatikken for et stort sprog som Java ellerC++. I al almindelighed viser det sig, at man ved shift-reduce-konflikter skal shifte fremforat reduce.

Men læseren bør være advaret om, at der ingen automatik er i dette: Støder man pa enshift-reduce-konflikt, skyldes det nok nærmere, at grammatikken er forkert udformet, end atman er i ovenstaende situation. Kun efter grundige overvejelser bør man læse shift-reduce-konflikter ved at shifte.

Reduce-reduce-konflikter, som heldigvis er sjældne, skyldes altid fejl i grammatikken.

Opgaver

Opgave 5.1 Konstruer LR(0)-DFA’en og LR(0)-parsetabellen for følgende grammatik:

0: S′ → S$1: S → aA

2: S → bB

3: A → cA

4: A → d

5: B → cB

6: B → d

Pars endvidere strengene accd og bcd.

Opgave 5.2 Konstruer LR(0)-DFA’en og LR(0)-parse-tabellen for følgende grammatik:

0: S′ → S$1: S → aAc

2: S → b

3: A → aSc

4: A → b

Opgave 5.3 Konstruer LR(0)-DFA’en for nedenstaende grammatik og vis, at den ikke erLR(0):

65

Page 66: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

0: S′ → S$1: S → aAd

2: S → bBd

3: S → aBe

4: S → bAe

5: A → c

6: B → c

Konstruer derefter SLR(1)-parsetabellen og vis, at grammatikken heller ikke er SLR(1).

Opgave 5.4 Betragt grammatikken:

0: S′ → S$1: S → S + a

2: S → a

Hvilket sprog frembringer grammatikken?Konstruer LR(0)-DFA’en for denne grammatik, vis at den ikke er LR(0), men derimod

SLR(1), og konstruer SLR(1)-parse-tabellen.

Opgave 5.5 Nedenstaende grammatik genererer C-udtryk med assignments og pointer-referencer.

0: S′ → S$1: S → V = E

2: S → E

3: E → V

4: V → x

5: V → ∗E

a) Konstruer LR(0)-DFA’en for denne grammatikb) Opskriv LR(0)- og SLR(1)-parsetabellerne, og find konflikternec) Konstruer LR(1)-DFA’en og opskriv LR(1)-parsetabellend) Konstruer LALR(1)-DFA’en og opskriv LALR(1)-parsetabellen

Opgave 5.6 Vis, at nedenstaende grammatik er LR(1), men ikke LALR(1):

0: S′ → S$1: S → aAd

2: S → bBd

3: S → aBe

4: S → bAe

5: A → c

6: B → c

Opgave 5.7 Vis, at nedenstaende grammatik er LALR(1), men ikke SLR(1):

0: S′ → S$1: S → Ma

66

Page 67: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

2: S → bMc

3: S → dc

4: S → dba

5: M → d

Opgave 5.8 Betragt grammatikken:

0: S′ → S$1: E → (L)2: E → a

3: L → L,E

4: L → E

a) Konstruer LR(0)-DFA’en

b) Konstruer SLR(1)-parsetabellen

c) Pars strengen ((a), a, (a, a))

d) Er dette en LR(0)-grammatik?

Opgave 5.9 Betragt grammatikken:

0: S′ → A$1: A → A(A)2: A → ε

a) Konstruer LR(0)-DFA’enb) Konstruer SLR(1)-parsetabellenc) Pars strengen (()())d) Er dette en LR(0)-grammatik?

Opgave 5.10 Betragt grammatikken:

0: S′ → S$1: E → (L)2: E → a

3: L → EL

4: L → E

a) Konstruer LR(0)-DFA’enb) Konstruer SLR(1)-parsetabellenc) Pars strengen ((a)a(aa))d) Er dette en LR(0)-grammatik?e) Konstruer LALR(1)-DFA’en uden at konstruere LR(1)-DFA’en først, og opskriv LALR(1)-

parsetabellen

Opgave 5.11 Betragt grammatikken:

67

Page 68: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

0: S′ → S$1: S → aSa

2: S → ε

Vis, at denne ikke er LR(1). Omskriv grammatikken, sa vi far en LR(1)-grammatik

68

Page 69: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

6 Det abstrakte syntaks-træ

I dette kapitel ser vi nærmere pa, hvorledes man i praksis far genereret parse-træet — dvs.det viser sig, at vi i virkeligheden ikke er særligt interesseret i selve parse-træet, men derimodi en simplificeret version, nemlig det abstrakte syntaks-træ.

Vi starter med at se pa parsergeneratoren CUP og pa, hvorledes grammatikken for Tinynemt kan transformeres til en parser. Dernæst skal det abstrakte syntaks-træ introduceres, ogder skrives Java-kode til dets implementering. Endelig knyttes de to dele sammen: Gennemsakaldte semantiske handlinger far vi den CUP-genererede parser til at generere det abstraktesyntakstræ fikst og færdigt.

6.1 Parsergeneratorer

Idet det, som kapitel 4 og 5 har vist, er en noget møjsommelig proces at skrive en parser ihanden, vælger man næsten altid at generere parseren automatisk vha. en sakaldt parserge-nerator.

Den mest udbredte parsergenerator er YACC (Yet Another Compiler-Compiler), som, udfra en specifikation af grammatikken genererer C-kode til en LALR(1)-parser. YACC findesi utallige varianter, og er overført til adskillige platforme og sprog. Vi skal lidt senere senærmere pa CUP (Construction of Useful Parsers), en Java-version af YACC.

Andre alternativer er ANTLR, som genererer Java-koden til en LL(k)-parser (og hvorlængden af lookahead k kan være vilkarlig stor), og JavaCC, som genererer en recursivedescent-parser. Da det er lidt besværligt at konstruere det abstrakte syntaks-træ i en top-down-parser, ser vi bort fra ANTLR og JavaCC og holder os til CUP.

Eksempel 61Vi vil vise, hvorledes man skriver CUP-specifikationen af grammatikken fra eksempel30:

Inputtet til CUP er en tekstfil, hvori man skriver, hvilke terminaler og non-terminaler,der findes. Derefter fastlægges startsymbolet, og endelig kommer alle produktionerne.

terminal X, LPAREN, RPAREN, PLUS, TIMES;

non terminal E, T, F;

start with E;

E ::= E PLUS T | T;

T ::= T TIMES F | F;

F ::= X | LPAREN E RPAREN;

Bemærk, at i CUP erstattes → af ::=.Nar CUP genererer LALR(1)-parseren, sa fremkommer der faktisk to filer, nemlig

parser.java , som er selve parseren, og sym.java , som i dette tilfælde far udseendet:

public class sym {

/* terminals */

69

Page 70: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

public static final int RPAREN = 4;

public static final int error = 1;

public static final int PLUS = 5;

public static final int LPAREN = 3;

public static final int TIMES = 6;

public static final int EOF = 0;

public static final int X = 2;

}

Disse definitioner kan snildt anvendes af JLEX.

Nedenfor er vist inputtet til CUP for Tiny:

terminal IF, THEN, ELSE, END, REPEAT, UNTIL, READ, WRITE, ASSIGN;

terminal EQ, LT, GT, PLUS, MINUS, TIMES, DIVIDE, LPAREN, RPAREN;

terminal SEMI, NUM, ID;

non terminal program, stmt_sequence, statement, if_stmt,;

non terminal repeat_stmt, assign_stmt, read_stmt,;

non terminal write_stmt, exp, comparison_op, simple_exp;

non terminal addop, term, multop, factor;

start with program;

program ::= stmt_sequence

;

stmt_sequence ::= statement SEMI stmt_sequence

| statement

;

statement ::= if_stmt

| repeat_stmt

| assign_stmt

| read_stmt

| write_stmt

;

if_stmt ::= IF exp THEN stmt_sequence END

| IF exp THEN stmt_sequence ELSE stmt_sequence END

;

repeat_stmt ::= REPEAT stmt_sequence UNTIL exp

;

assign_stmt ::= ID ASSIGN exp

;

read_stmt ::= READ ID

;

write_stmt ::= WRITE ID

;

exp ::= simple_exp comparison_op simple_exp

70

Page 71: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

| simple_exp

;

comparison_op ::= LT

| GT

| EQ

;

simple_exp ::= simple_exp addop term

| term

;

addop ::= PLUS

| MINUS

;

term ::= term multop factor

| factor

;

multop ::= TIMES

| DIVIDE

;

factor ::= LPAREN exp RPAREN

| NUM

| ID

;

Grunden til, at hver enkelt produktion star pa sin egen linie, er, at vi senere vil tilføjeen sakaldt semantisk handling til hver produktion — en stump Java-kode, der er med til atkonstruere det abstrakte syntakstræ.

JLex kan sættes til at anvende definitionerne fra sym.java :

%%

%{

private java_cup.runtime.Symbol tok(int k, Object value) {

return new java_cup.runtime.Symbol(k, yychar,

yychar+yylength(), value);

}

%}

%function nextToken

%type java_cup.runtime.Symbol

%char

%eofval{

{return tok(sym.EOF,null);}

%eofval}

digit [0-9]

number {digit}+

letter [a-zA-Z]

identifier {letter}+

71

Page 72: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

%%

"if" {return tok(sym.IF,null);}

"then" {return tok(sym.THEN,null);}

"else" {return tok(sym.ELSE,null);}

"end" {return tok(sym.END,null);}

"repeat" {return tok(sym.REPEAT,null);}

"until" {return tok(sym.UNTIL,null);}

"read" {return tok(sym.READ,null);}

"write" {return tok(sym.WRITE,null);}

":=" {return tok(sym.ASSIGN,null);}

"=" {return tok(sym.EQ,null);}

"<" {return tok(sym.LT,null);}

">" {return tok(sym.GT,null);}

"+" {return tok(sym.PLUS,null);}

"-" {return tok(sym.MINUS,null);}

"*" {return tok(sym.TIMES,null);}

"/" {return tok(sym.DIVIDE,null);}

"(" {return tok(sym.LPAREN,null);}

")" {return tok(sym.RPAREN,null);}

";" {return tok(sym.SEMI,null);}

{number} {return tok(sym.NUM,new Integer(yytext()));}

{identifier} {return tok(sym.ID, yytext());}

(" "|\n|\t|\n\r|\r\n)+ {}

For yderligere information om CUP henvises til manualen.

6.2 Det abstrakte syntakstræ

Det viser sig, at parse-træet i al almindelighed vil indeholde alt for meget overflødig informa-tion til, at det er bekvemt at arbejde med videre i compileringen. Lad os give et eksempel padette:

Eksempel 62Betragt LL-grammatikken fra eksempel 30

E → TE ′ E ′ → +TE ′|ε T → FT ′ T ′ → ∗FT ′|ε F → x|(E)som jo oprindeligt kom fra denne grammatik:

E → E + T |T T → T ∗ F |F F → x|(E)men hvor alle de ekstra non-terminaler blev indført for at fjerne tvetydighed og venstre-rekursion.

Betragt strengen x + x ∗ x. Denne har parsetræet pa figur 27, men i virkelighedenkunne vi nøjes med et af træerne pa figur 28.

Til venstre pa figur 28 er vist parse-træet efter den oprindelige grammatik, mens vi tilhøjre har syntaks-træet for udtrykket. Det vigtige er her at observere, at alle tre træer

72

Page 73: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

S

E

T E'

x �

F T' + T E'

F T'x*

F T'

x

Figur 27

E

E + E

x E * E

x x

+

x *

x x

Figur 28

indeholder nøjagtigt den samme information, og det ville derfor være en fordel, hvis vikunne nøjes med et træ noget i stil med de to nederste i den senere compilering.

Ideen bag det abstrakte syntakstræ er at fa parseren til at generere — ikke den konkreteparse-træ, for det har vi ikke noget at bruge til — men derimod et simplificeret syntakstræ,der lige præcist indeholder den nødvendige information.

I det abstrakte syntakstræ vil de fleste terminaler ikke være repræsenteret:Ser vi f.eks. pa Java, vil vi ikke fa brug for at føre klammer, ’{’ og ’}’, over i det abstrakte

syntakstræ, idet klammernes rolle er at vise, hvor i kildekoden en blok starter og slutter.Denne information findes allerede i det abstrakte syntakstræ, idet indholdet af blokken vilvære det deltræ, der har en passende knude som rod.

Semikoloner er heller ikke nødvendige, idet deres opgave er at adskille statements fra hin-anden, men dette er jo allerede gjort i det abstrakte syntakstræ.

I praksis er det lidt af en kunst at specificere den abstrakte grammatik for et programme-ringssprog, idet man lige præcist skal have den nødvendige information med, men heller ikkemere. Heldigvis behøver den abstrakte grammatik ikke at være utvetydig, da det jo ikke erden, men derimod den konkrete grammatik, vi parser efter.

73

Page 74: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

I det følgende vil det abstrakte syntakstræ blive forkortet AST.

6.3 Implementering af AST

Inden vi far CUP til at generere AST’et for Tiny, skal vi beskrive en abstrakt grammatik forTiny samt vise, hvorledes et AST kan implementeres.

Vi husker pa fra kapitel 3.3, at Tiny har den konkrete grammatik:

program → stmt-seq

stmt-seq → statement ; stmt-seq | statement

statement → if-stmt | repeat-stmt | assign-stmt | read-stmt | write-stmt

if-stmt → if exp then stmt-seq end | if exp then stmt-seq else stmt-seq end

repeat-stmt → repeat stmt-seq until exp

assign-stmt → identifier := exp

read-stmt → read identifier

write-stmt → write identifier

exp → simple-exp comparison-op simple-exp | simple-exp

comparison-op → > | < | =simple-exp → simple-exp addop term | term

addop → + | −term → term multop factor | factor

multop → * | /

factor → ( exp ) | number — identifier

I den abstrakte grammatik arbejder vi kun med 3 typer terminaler, nemlig textbfidentifier,textbfnumber og textbfbinop. De to første har deres sædvanlige betydning, mens textbfbinoper et af symbolerne ’<’, ’>’, ’=’, ’+’, ’*’, ’−’, ’/’. Vi repræsenterer dog ikke selve symbolet,men nøjes med en talkode.

I modstrid med konventionerne lader vi alle attributterne i AST-klasserne være public —det er maske ikke pæn objektorienteret programmering, men det gør det nemmere at skrivekode senere.

Koden til klassen Binop.java bliver

public class Binop {

public static int LT = 0;

public static int GT = 1;

public static int EQ = 2;

public static int PLUS = 3;

public static int MINUS = 4;

public static int TIMES = 5;

public static int DIVIDE = 6;

}

74

Page 75: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Endvidere far vi brug for klasserne Identifier og Numeral (ikke Number , det er deren anden Java-klasse, der hedder).

I den abstrakte grammatik far vi kun brug for non-terminalerne stmt-seq, stmt og exp. Hveraf disse non-terminaler vil i implementeringen give en abstrakt superklasse, og hver enkeltproduktion med non-terminalen pa venstre side vil give en subklasse hertil. Vi navngiverderfor vores produktioner. (Dette er dog ikke nødvendigt ved stmt-seq, da vi kun har enproduktion.)

(StmtSeq) stmt-seq → statement stmt-seq(IfStmt) stmt → exp stmt-seq stmt-seq(RepeatStmt) stmt → stmt-seq exp(AssignStmt) stmt → identifier exp(ReadStmt) stmt → identifier(WriteStmt) stmt → identifier(BinopExp) exp → exp binop exp(NumberExp) exp → number(IdExp) exp → identifier

Ved StmtSeq behøver vi ikke tage specielt højde for situationen, hvor sekvensen stopper —vi kan nemlig bare lade stmt-seq ’en pa højresiden være null.

Tilsvarende for IfStmt — har vi ikke en else, sa lader vi bare den sidste non-terminal værenull.

Selve AST-klasserne bliver nu:

public abstract class AST {}

public class StmtSeq extends AST {

public Stmt stmt;

public StmtSeq next;

public StmtSeq(Stmt stmt, StmtSeq next) {

this.next = next;

this.stmt = stmt;

}

}

public abstract class Stmt extends AST { }

public class IfStmt extends Stmt {

public Exp condition;

public StmtSeq ifBody;

public StmtSeq elseBody;

public IfStmt(Exp condition, StmtSeq ifBody, StmtSeq elseBody) {

this.condition = condition;

this.ifBody = ifBody;

this.elseBody = elseBody;

}

}

75

Page 76: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

— og læseren kan sikkert selv gætte sig til resten. Disse klasser vil senere blive udvidetmed flere attributter og metoder.

6.4 Semantiske handlinger

Vi skal nu have CUP til at opbygge parsetræet. Dette sker ved hjælp af sakaldte semantiskehandlinger:

En semantisk handling er en metode, der kaldes af parseren, hver gang der udføres enreduce-operation i LR-parseren. Ideen er, at vi til hver eneste symbol i vores grammatiktilknytter et deltræ af AST’et, og hver gang en reduce-operation udføres, konstruerer vi udfra AST’erne pa højresiden af reduce-produktionen et AST for den non-terminal, der star pavenstresiden. Dette er muligt, idet LR-parseren er en bottom-up parser, sa alle AST’erne pahøjresiden er allerede konstruerede.

Eksempelvis vil en reduce efter produktionenif-stmt → if exp then stmt-seq else stmt-seq end

tildele non-terminalen pa venstre side af AST’et new IfStmt(exp, stmtSeq1, stmtSeq2),hvor exp er AST’et for exp, stmtSeq1 er AST’et for den første stmt-seq pa højresiden etc.

I praksis foregar dette ved, at hver enkelt non-terminal og visse terminaler i CUP tildelesen type, f.eks. Stmt , og at vi endvidere ud for hver enkelt produktion skriver den tilsvarendesemantiske handling.

Detaljerne er overvældende — og ikke specielt interessante — og læseren opfordres til atbetragte vedlagte CUP-fil, men her er et par detaljer:

parser code {:

public AST parseResult;

Yylex lexer;

public parser(Yylex tl) {

lexer = tl;

}

:};

scan with {: return lexer.nextToken(); :};

terminal IF, THEN, ELSE, END, REPEAT, UNTIL, READ, WRITE, ASSIGN;

...

terminal Integer NUM;

terminal String ID;

non terminal AST program;

non terminal StmtSeq stmt_seq;

non terminal Stmt statement, if_stmt, ....

non terminal Exp exp, simple_exp, term, factor;

non terminal Integer comparison_op, addop, multop;

start with program;

program ::= stmt_seq:ss

76

Page 77: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

{:RESULT = parser.parseResult = ss; :}

;

stmt_seq ::= statement:s SEMI stmt_seq:ss

{:RESULT = new StmtSeq(s,ss); :}

| statement:s {:RESULT = new StmtSeq(s,null); :}

;

statement ::= if_stmt:s {:RESULT = s; :}

| repeat_stmt:s {:RESULT = s; :}

| assign_stmt:s {:RESULT = s; :}

| read_stmt:s {:RESULT = s; :}

| write_stmt:s {:RESULT = s; :}

;

assign_stmt::= ID:i ASSIGN exp:e

{:RESULT = new AssignStmt(new Identifier(i), e); :}

;

Bemærk, at en del af de semantiske handlinger er noget trivielle — f.eks. videregiver pro-duktionerne med statement bare sin del af AST’et.

Variablen parser.parseResult er vigtig, idet det er en reference til roden af AST’et, og deter den, vi bruger videre i den semantiske analyse.

6.5 Traversering af AST

I den semantiske analyse far vi brug for at traversere AST’et, og vi skal derfor skrive kode,der understøtter dette.

Man skal nu overveje, hvilken klasse, der skal have ansvaret for at traversere den enkelteknude i AST’et. Ifølge traditionelt objekt-orienteret design ville man nok lade den enkelteknude have dette ansvar, men det viser sig at være uhensigtsmæssigt:

I den semantiske analyse og videre forekommer traverseringerne i forbindele med specifikkeopgaver, sa som udfyldelse af symboltabellen, generering af mellemrepræsentations-kode, etc.Disse faser er meget væsensforskellige, og er ikke altid fastlagt, nar man begynder at skrivecompileren (f.eks. generering af kode til vores malprocessor). Det er derfor smart at samle enenkelt fases kode i en klasse i stedet for at sprede den ud over de mange AST-klasser — detbliver lettere at skrive koden, og lettere at vedligeholde koden. Alt dette kan bl.a. gøres vha.det sakaldte Visitor-design-mønster.

Vi starter med at skrive et interface, Visitor :

public interface Visitor {

public Object visitIdentifier(Identifier node, Object obj);

public Object visitNumeral(Numeral node, Object obj);

public Object visitStmtSeq(StmtSeq node, Object obj);

public Object visitIfStmt(IfStmt node, Object obj);

public Object visitRepeatStmt(RepeatStmt node, Object obj);

public Object visitAssignStmt(AssignStmt node, Object obj);

public Object visitReadStmt(ReadStmt node, Object obj);

public Object visitWriteStmt(WriteStmt node, Object obj);

public Object visitBinOpExp(BinOpExp node, Object obj);

77

Page 78: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

public Object visitNumberExp(NumberExp node, Object obj);

public Object visitIdExp(IdExp node, Object obj);

}

Der skal være en (abstrakt) metode for hver enkelt klasse, som kan forekomme i AST’et.Metoderne skal modtage to argumenter, nemlig selve knuden i AST’et, og et argument, obj, ogmetoderne skal returnere et Object . Disse to objekter kan bruges til at modtage og returnereinformation.

Hver enkelt AST-klasse udvides med en metode, visit, som med det samme kalder dentilsvarende metode i Visitor :

public class BinOpExp extends Exp {

public Exp exp1;

public int operator; // constant from Binop

public Exp exp2;

public BinOpExp(Exp exp1, int operator, Exp exp2) {

this.exp1 = exp1;

this.operator = operator;

this.exp2 = exp2;

}

public Object visit(Visitor v, Object obj) {

return v.visitBinOpExp(this, obj);

}

}

Det virker lidt indviklet, men med dette framework har vi sikret os, at alt arbejdet foregari den konkrete visitor-klasse.

Lad os som eksempel — og for at give mulighed for at teste JLex- og CUP-filerne —skrive en visitor, der udskriver AST’et pa listeform. Ideen er, at vi i hver enkelt visit-metode ivisitoren udskriver en linie om selve knuden i AST’et, og dernæst besøger knudes efterkommere— der er altsa tale om en pre-order traversering.

Parameter-objektet er her en streng, som indeholder information om, hor mange mellem-rum, hver enkelt streng skal indrykkes — det gør det lettere at læse resultatet.

Filen TreePrintVisitor.java bliver (i uddrag):

public class TreePrintVisitor implements Visitor {

String obj;

public TreePrintVisitor() {

obj = "";

}

public Object visitIdentifier(Identifier node, Object obj){

System.out.println(obj + "Identifier:" + node.name);

return null;

}

public Object visitNumeral(Numeral node, Object obj){

System.out.println(obj + "Numeral:" + node.value);

78

Page 79: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

return null;

}

public Object visitStmtSeq(StmtSeq node, Object obj){

System.out.println(obj + "StatementSeq");

node.stmt.visit( this, obj + " ");

if (node.next != null)

node.next.visit( this, obj + " ");

else System.out.println(obj + "intet");

return null;

}

public Object visitIfStmt(IfStmt node, Object obj){

System.out.println(obj + "IfStatement");

node.condition.visit( this, obj+" ");

node.ifBody.visit( this, obj + " ");

if (node.elseBody != null)

node.elseBody.visit( this, obj + " ");

return null;

}

....

og for at teste programmet skal man køre TreeTester.java :

import java.io.*;

public class TreeTester {

public TreeTester(String filename) {

InputStream inp;

try {

inp=new FileInputStream(filename);

} catch (FileNotFoundException e) {

throw new Error("File not found: " + filename);

}

parser p = new parser(new Yylex(inp));

try {

p.parse();

} catch (Throwable e) {

e.printStackTrace();

throw new Error(e.toString());

} finally {

try {

inp.close();

} catch (IOException e) {}

}

TreePrintVisitor tpv = new TreePrintVisitor();

p.parseResult.visit(tpv, " ");

}

public static void main(String argv[]) {

String filename = "fakultet.tiny";//argv[0];

79

Page 80: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

new TreeTester(filename);

}

}

Resultatet af at køre dette pa Tiny-programmet fra kapitel 1.3 bliver;

StatementSeq

ReadStatement

Identifier:x

StatementSeq

IfStatement

BinopExp

IdExp

Identifier:x

Operator1

NumberExp

Numeral:0

StatementSeq

AssignStatement

Identifier:fact

NumberExp

Numeral:1

hvor resten af træet udelades.

Opgaver

Opgave 6.1 Skriv en visitor, som traverserer AST’et og laver en liste over samtlige identifiersi Tiny-programmet. Gem f.eks. identifierne i en Vector .

Opgave 6.2 Skriv en visitor, som traverserer et Tiny-AST og udskriver den tilsvarende Java-kode.

(Vink: Da Tiny ikke deklarerer sine variable, mens Java kræver deklarationer, sa er det noknemmest at anvende resultatet fra opgave 6.1, og starte Java-filen med en række deklarationer.Du kan f.eks. anvende JOptionPane.showInputDialog() til read-sætningerne).

Opgave 6.3 Udvid Tiny med for- og while-sætninger som i Java.

Opgave 6.4 Lav JLex-, CUP- og AST-filer for C-.

80

Page 81: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

7 Attribut-grammatikker

Vi skal nu formalisere opbygningen af det abstrakte syntakstræ i forrige kapitel samt opstilleet framework, indenfor hvilken de forestaende traverseringer af AST’et kan forega.

Der findes flere mader, hvorpa et sadant formalistisk framework kan laves — vi vælger denmest udbredte, nemlig attribut-grammatikker.

7.1 Attributter og attribut-grammatikker

Ideen bag attribut-grammatikker stammer fra princippet om syntaks-styret semantik : I allemoderne programmeringssprog lader vi parsetræet (eller AST’et) bestemme semantikken —der forekommer ingen semantisk information, som ikke kan udledes fra den generelle syntaksog det faktiske parsetræ eller AST.

I praksis bestemmes semantikken ved at tilføje forskellige attributter til hver enkelt knudei AST’et. Ud fra nogle fastlagte startværdier for disse attributter i enkle knuder (som regelterminaler) og nogle regler kan attributterne nu udregnes for alle knuder i træet.

Disse attributter kan være alt mellem himmel og jord. Abenlyse muligheder er følgende:Typen af en variabel, værdien af et udtryk, placeringen i computerens hukommelse af envariabel og malkoden (eller nok nærmere mellemrepræsentationskoden) af et delprogram.

I en attribut-grammatik tilknytter vi attributter til de forskellige symboler (terminalersavel som non-terminaler) i grammatikken, og til hver eneste produktion X0 → X1X2 . . . Xn

har vi nogle semantiske regler af formen:

Xi.ai = f(X0, a0, X1.a0, . . . X0.a1, X1.a1, . . . X0.an, . . . Xn.am)

Dette ser kompliceret ud, men formlen ovenfor fortæller os bare, at et af symbolernes attri-butter kan beregnes ud fra resten. I praksis vil de semantiske regler være ganske simple.

Vi anvender i øvrigt notationen X.a for værdien af a-attributten for symbolet X.

Eksempel 63Som eksempel pa en simple attribut-grammatik betragter vi nedenstaende grammatik,der frembringer ikke-negative, hele tal:

number → number digit | digit digit→ 0|1|2|3|4|5|6|7|8|9Til hver enkelt symbol i grammatikken tildeler vi en attribut, val, som skal repræsentereværdien af symbolet.

For produktionerne digit→ num etc. har vi reglerne:

digit→ 0 digit.val = 0digit→ 1 digit.val = 1digit→ 2 digit.val = 2digit→ 3 digit.val = 3digit→ 4 digit.val = 4digit→ 5 digit.val = 5digit→ 6 digit.val = 6digit→ 7 digit.val = 7digit→ 8 digit.val = 8digit→ 9 digit.val = 9

81

Page 82: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

For non-terminalen number er sagen lidt mere kringlet: Den ene produktion number →digit er nem nok, idet vi har number.val = digit.val, men for den anden produktioner der to forekomster at number — disse skelner vi fra hinanden ved at tildele dem etsubscript. Reglen bliver derfor:

number1 → number2 digit number1.val = number2.val ∗ 10 + digit.val

Begrundelsen herfor er, at nar vi sætter et ekstra ciffer i enden pa et tal, sa rykker allede gamle cifre en plads op i titalssystemet, og deres værdi multipliceres derfor med 10.

Lad os illustrere parsetræet for strengen ”273”— parsetræet er dekoreret med attribut-beregningerne for val pa figur29

digit( = 2)val

digit( = 7)val

digit( = 3)val

2

7

3number( = 2)val

number( = 2*10+7=27)val

number( = 27*10+3=273)val

Figur 29

Ovenstaende eksempel er maske lidt fjollet — i praksis vil man lade lexeren beregne værdienaf numeriske udtryk som strengen ”273”.

Eksempel 64Betragt grammatikken fra kapitel 3 for aritmetiske udtryk:

E → E + T |T T → T ∗ F |F F → x|(E)Udvides denne grammatik med attributten val, som angiver værdien af udtrykket, fasnedenstaende semantiske regler — vi antager, at værdien af x er givet pa forhand:

E1 → E2 + T E1.val = E2.val + T.valE → T E.val = T.valT1 → T2 ∗ F T1.val = T2.val + F.valT → F T.val = F.valF → x F.val = x.valF → (E) F.val = E

82

Page 83: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Bemærk, at det ville være temmeligt svært at opskrive tilsvarende regler for grammatikken,hvor vi har fjernet venstre-rekursionen — det er ikke umiddelbart indlysende at se, hvadbetydningen af E ′.val og T ′.val er.

Dette er en af grundene til, at vi ikke arbejder yderligere med LL-parsere — fjernelsen afvenstre-rekursion giver i almindelighed non-terminaler med uklar semantisk betydning.

Eksempel 65De semantiske regler fra kapitel 6.4, som vi brugte til at generere AST’et for Tiny, kannemt formuleres i termer af en attribut-grammatik: Til hvert symbol tilknytter vi enattribut tree, som er et AST .

Saledes giver produktionenif-stmt → if exp then stmt-seq1 else stmt-seq2

følgende semantiske regel:if-stmt.tree = new IfStmt( exp.tree, stmt-seq1.tree, stmt-seq2.tree)

type( = )dtype real

float

decl

var-list

var-listid (x) ,

( = )dtype real ( = )dtype real

( = )dtype real

( = )dtype real

id (y)

Figur 30

Eksempel 66Lad os betragte et noget mere kompliceret eksempel: Nedenstaende grammatik

decl → type var-listtype → int | floatvar-list → id , var-list | idgenererer variabel-deklarationer som i C eller Java.Vi vil undersøge en attribut, dtype, som angiver typen (integer eller real) for en

variabel. (Vi kalder attributten for dtype for ikke at forveksle den med non-terminalentype).

Attribut-grammatikken bliver:

83

Page 84: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

decl → type var-list var-list.dtype = type.dtypetype → int type.dtype = integertype → float type.dtype = realvar-list1 → id , var-list2 id.dtype =var-list1.dtype

var-list2.dtype = var-list1.dtypevar-list → id id.dtype = var-list.dtype

Bemærk for det første, at selve deklarationen decl ikke tildeles nogen type — detteer simpelthen ikke nødvendigt.

Endvidere bør det bemærkes, at der til den 4. produktion faktisk er knyttet to se-mantiske regler.

Endelig gar beregningen af dtype bade opad og nedad i parsetræet og ikke kun opadsom i de foregaende eksempler. Betragt f.eks. parsetræet for strengen float x, y pafigur 30. Her gar beregningen af dtype fra type-knuden via decl -knuden til den højre delaf træet.

7.2 Synthetiserede og nedarvede attributter

Efter at man har specificeret sin attribut-grammatik, kommer sa problematikken: Hvorledesberegnes disse attributter?

I praksis er man sjældent i tvivl: Attributterne beregnes ved en (eller flere) traverseringeraf det abstrakte syntakstræ, og rækkefølgen af beregningerne i hver enkelt knude er som regelgivet umiddelbart af de semantiske regler.

Men der er alligevel en række problemer, der kan opsta. Vi gennemgar derfor en del afteorien for beregninger i attribut-grammatikker.

Historisk set har denne teori ogsa interesse: Tidligere arbejdede man med sakaldte single-pass compilere, hvor en del af kildekoden blev indlæst, lexet, parset, etc., og tilsidst oversattil noget malkode, hvorefter en ny stump kildekode blev bearbejdet.

Begrundelsen for dette var, at ”i gamle dage”havde computere ikke specielt meget hukom-melse, og som regel ikke nok til at kunne repræsentere hele AST’et. Endvidere var single-passcompilering meget hurtigere end multiple-pass.

Nu om dage er situationen lige omvendt: Computere har rigeligt med hukommelse (seti betragtning af, at det typiske program, modul eller klasse, der skal compileres, ikke ervæsentligt større end for 20 ar siden), og hastighed er heller ikke noget større problem. Mananvender derfor multiple-pass compilere, hvor især AST’et gennemløbes flere gange, førendmalkoden produceres.

Fordelen ved dette er især, at man far et meget mere fleksibelt sprog. F.eks. kan nævnes,at i ældre programmeringssprog, som Pascal, skulle en procedure deklareres, før den kunnekaldes. Sakaldte forward-deklarationer var derfor hyppigt forekommende. I Java og andremoderne programmeringssprog er metodernes rækkefølge fuldkommen ligegyldig, hvilket eren stor bekvemelighed.

Definition 67En attribut er synthetiseret, hvis værdien af attributten i en knude i AST’et kun afhæn-ger af attributterne i knudens efterfølgere.

En attribut er nedarvet, hvis værdien af attributten i en knude i AST’et afhænger afattributterne i knudens forfader eller søskende-knuder.

84

Page 85: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Det ses, at attributterne i de tre første eksempler, 63–65 alle er synthetiserede, mens dtypei det sidste eksempel er nedarvet.

Synthetiserede attributter er specielt interessante, idet de kan beregnes bottom-up samti-digt med genereringen af AST’et. Dette udnyttes i de føromtalte single-pass compilere.

Definition 68En attribut-grammatik, hvori alle attributter er synthetiserede, kaldes en attribut-grammatikaf type S.

En attribut-grammatik, hvori alle nedarvede attributter opfylder nedenstaende betin-gelse, kaldes en attribut-grammatik af type L : For enhver produktion X0 → X1X2 . . . Xn

er de semantiske regler af formen

Xi.ai = f(X0.a1, . . . Xi−1.a1, X0.a2, . . . Xi−1.a2, . . . , X0.am, . . . Xi−1.am)

dvs. værdien af attributterne afhænger kun af enten forfaderens attributter eller afattributterne hos de søskende, der star til venstre for knuden.

Det viser sig, at man nemt kan fa en bottom-up-parser til at beregne alle attributterne iattribut-grammatikker af type S eller L, samtidigt med, at man konstruerer AST’et.

Da traverseringer af AST’et er dyre i tid, vil man i praksis derfor foretrække at arbejdemed f.eks. type L-grammatikker og beregne sa mange attributter som muligt under parsingen.

Vi vil dog af pædagogiske grunde undlade attribut-beregner under parsingen og istedettraversere AST’et adskillige gange.

7.3 Algoritmer til beregning af attributter

Vi skal nu se pa, hvorledes man bestemmer rækkefølgen af beregningerne af attributter. Dettesker ved at betragte sakaldte afhængighedsgrafer.

I en afhængighedsgraf svarer knuderne til attributter, mens der er en orienteret kant fraattribut a til attribut b, hvis a direkte har indflydelse pa beregningen af b.

En afhængighedsgraf er saledes en orienteret graf.I praksis starter vi med at tegne parsetræet, ved siden af hver knude i parse-træet skrive

knudens attributter, og dernæst sætte afhængighedspilene pa.Afhængighedsgrafen for parsetræet i eksempel 63 bliver som pa figur 31.Afhængighedsgrafen for parsetræet i eksempel 66 bliver som figur 32Til skræk og advarsel har vi endnu et eksempel:

Eksempel 69Betragt attribut-grammatikken:

S → AB S.u = S.vA.u = S.uB.u = S.vB.v = S.uS.v = B.u+B.v

A→ a A.v = 2B → b

85

Page 86: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

digit

digit

digit

2

7

3number

number

valnumber

val val

val val

val

Figur 31

type

float

decl

var-listdtype

var-listid ( )x ,

id ( )y

dtype

dtypedtype

dtype

Figur 32

Afhængighedsgrafen bliver som pa figur 33. Som det ses, er der noget galt — S.uafhænger af B.v, som afhænger af A.u, som afhænger af S.u . . . Vi har en cykel iafhængighedsgrafen!

I almindelighed ma der ikke være cykler i afhængighedsgrafen — problemet med cykler er,at hvis de optræder, sa ved vi ikke, hvilken attribut, vi skal starte med at beregne. Derforskal afhængighedsgrafen for an attribut-grammatik altid være en orienteret, acyklisk graf.(forkortet DAG (directed acyclic graph).

Heldigvis findes der en algoritme til at kontrollere, om en orienteret graf faktisk er acyklisk— og tilmed angiver algoritmen en rækkefølge, hvori attributterne kan beregnes. Algoritmenhedder topologisk sortering.

Vi skal ikke gennemga topologisk sortering i detaljer — hertil henvises til f.eks. [ASU] —men blot skitsere algoritmen:

Vi finder iterativt en knude, der ikke er efterfølger til nogen anden knude, og fjerner denneknude.

Denne proces fortsætter indtil der ikke er flere knuder tilbage — og rækkefølgen, hvori

86

Page 87: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

A

a

S

B

b

u v

u v u v

Figur 33

knuder blev udtaget, er netop beregningsrækkefølgen.Stopper processen, selvom der stadigvæk er knuder tilbage i grafen, sa er det fordi der er

en cykel i grafen.

Topologisk sortering er især interessant, nar man vil undersøge, om ens attribut-grammatikfaktisk har en afhængighedsgraf, som er en DAG.

Man kan naturligvis ogsa anvende topologis sortering til at finde en rækkefølge at traversereAST’et i, men i praksis er de fleste attribut-grammatikker ret simple, og rækkefølgen er retindlysende.

Opgaver

Opgave 7.1 Skriv en attribut-grammatik for nedenstaende grammatik. Attributten skal re-præsentere værdien af det frembragte heltal.

number → digit number|digit digit → 0|1|2|3|4|5|6|7|8|9(Vink: Man far brug for mindst to attributter — selve værdien og en attribut, som fortæller,hvor mange cifre der er i number)

Opgave 7.2 Skriv en attribut-grammatik for nedenstaende grammatik, som frembringer de-cimaltal. Attributten skal repræsentere værdien af det frembragte decimaltal.

dnum → num.num num → num digit|digit digit → 0|1|2|3|4|5|6|7|8|9

Opgave 7.3 I Pascal deklareres variable pa følgende made: x, y, z:integer; deklarerervariablerne x, y og z til at være heltal. En grammatik for sadanne deklarationer er:

decl → var-list : type var-list → var-list, id|id type → integer|realSkriv en attribut-grammatik, som finder typen af en variabel.

Tegn parse-træet og afhængighedsgrafen for strengen x, y, z:integer; genereret af dennegrammatik.

Opgave 7.4 Tegn parse-træet og afhængighedsgrafen for strengen 3 ∗ (4 + 5) ∗ 6 genereret afgrammatikken i eksempel 64.

87

Page 88: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Opgave 7.5 Betragt nedenstaende attribut-grammatik:S → ABC B.u = S.u

A.u = B.v + C.vS.v = A.v

A → a A.v = 2 ·A.uB → b B.v = B.uC → c C.v = 1

a) Tegn parsetræet for strengen abc, og tegn den tilsvarende afhængighedsgraf.b) Angiv en rækkefølge, hvori de forskellige attributter fra opgave a) kan beregnes.c) Antag, at S.u = 3. Hvad er værdien af S.v ?

Opgave 7.6 Betragt nedenstaende attribut-grammatik:S → ABC B.u = S.u

A.u = B.v + C.vC.u = A.v

A → a A.v = 2 ·A.uB → b B.v = B.uC → c C.v = C.u− 2

a) Tegn parsetræet for strengen abc, og tegn den tilsvarende afhængighedsgraf.b) Angiv en rækkefølge, hvori de forskellige attributter fra opgave a) kan beregnes.c) Antag, at S.u = 3. Hvad er værdien af S.v ?

88

Page 89: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

8 Symboltabellen

Den vigtigste datastruktur i compileren er, næst efter det abstrabte syntakstræ, symboltabel-len. Dennes opgave er at holde styr pa samtlige identifiers i det program, der skal compileres,herunder identifierens type, eventuelle placering i hukommelsen mm. Endvidere bruger visymboltabellen til at kontrollere, at alle anvendte variable faktisk er blevet deklareret, ogunder det sakaldte typecheck kontrollerer vi ogsa, at variablerne anvendes i overensstemmelsemed deres type.

I symboltabellen kan der indsættes op til fire forskellige typer af identifiers: Konstanter,variable, typer og procedurer, funktioner eller metoder

I et sprog som Java er der principielt ingen forskel mellem konstanter og variabler — enkonstant er en variabel, der er deklareret som final, og dette betyder, at konstanten kantildeles en værdi en gang, og derefter beholder den denne værdi. Men i andre sprog som f.eks.Pascal skal værdien af en konstant være kendt pa compileringstidspunktet.

I Java kan man ikke umiddelbart definere sine egne typer, medmindre man vil ud i atdefinere en hel klasse. Men i andre sprog, f.eks. Pascal og C, har man rige mulighed for atoprette egne typer og navngive disse. Mere herom i kapitel 9.

Deklarationer af procedurer, funktioner, metoder, eller hvad man nu foretrækker at kaldedem, er i virkeligheden deklarationer af konstanter — og hvor værdien af konstanten er etstykke kildekode. Men handteringen af procedurer er en sag for sig, og dette omtales nærmerei kapitel 10.

I de fleste programmeringssprog befinder alle 4 slags identifiers sig i samme navnerum, meni visse sprog er der forskelle — f.eks. er det tilladt at have en variabel og en type, der hedderdet samme. Vi vil dog holde os til det tilfælde, hvor der kun er et navnerum.

Da der er en del finurligheder omkring scope og levetid, starter vi med at resumere dissebegreber.

8.1 Scope og levetid

Scope for en identifier angiver den del af programmet, hvori identifieren kan tilgas, menslevetiden fortæller, hvornar identifieren oprettes og nedlægges.

I de fleste compilerede programmeringssprog skal en variabel deklareres, før den kan an-vendes, mens der i mange fortolkede sprog gælder det modsatte. Dette er et klassisk trade-offmellem korrekthed og fleksibilitet. Betragt f.eks. nedenstaende kodestump:

for(int i = 0; i < 100; i++)

System.out.println(I);

(hvor det antages, at der ikke tidligere optræder en variabel I). Formentligt mente pro-grammøren ’i’ i stedte for ’I’ i den sidste linie. I et sprog som Java ville en sadan fejl blivefanget under compileringen — under den semantiske analyse — mens et fortolket sprog somJavaScript ikke ville løfte et øjenbryn, ja, ikke engang komme med en run-time-fejl — derimodville der blive oprettet en ny variabel ’I’.

Betragt følgende kodestump, skrevet i f.eks. Java eller C:

int i, j;

89

Page 90: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

int f(int size) {

char i, temp;

...

{

double j;

...

}

...

{

float j;

...

}

}

Her optræder der forskellige variable med navnene i og j. int’erne i og j er at betragtesom globale variable og er synlige i hele den klasse eller modul, vi er inden i — men ikke indei metoden f . Her optræder der nemlig en lokal variabel i af typen char, som ’overskygger’int’en i i hele f — vi siger, at int’en i har et scope hole her.

Tilsvarende kan int’en j ikke ses i den første blok inden i f , idet der her optræder endouble ved samme navn j, og ej heller i den anden blok.

Generelt er scope-reglerne i Java, C og de fleste andre sprog, at scope for en variabel erfra deklarationen af variablen til afslutningen af den blok (eller metode eller modul), hvorivariablen er deklareret. Der kan ikke deklareres to variable af samme navn pa samme niveaui samme blok, og deklarationer i indre blokke overskygger deklarationer i ydre blokke.

En konsekvens heraf er, at i en compiler for Java-lignende sprog har vi brug for adskilligesymboltabeller, nemlig en for hver enkelt blok. Vi skal senere se, hvorledes dette problemløses.

Man skelner ogsa mellem statisk scope (eller leksikalsk scope) og dynamisk scope. I statiskscope knyttes forekomsten af en variabel til den nærmeste foregaende deklaration (hvor nær-meste skal fortolkes i termer af indelukkende blokke), mens vi ved dynamisk scope finder enpassende deklaration ved at ga tilbage i programmets udførselshistorie, ved bl.a. at betragte,hvorfra vores metode blev kaldt.

Betragt følgende stump kode:

int i = 1; // global variabel

void f() {

System.out.println(i);

}

public static void main(String[] args) {

int i = 2;

f();

}

Ved den sædvanlige statiske scope udskriver programmet tallet 1 — variablen i i metodenf tilknyttes under compileringen til den globale deklaration af i og far værdien 1.

90

Page 91: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Ved dynamisk scope udskrives tallet 2 — vi ser først, om i er deklareret i f , og da denikke er det, gar vi tilbage til den kaldende metode, main, og anvender den heri deklareredevariabel i.

Dynamisk scope anvendes næsten ikke i moderne programmeringssprog: Dels gør afhæn-gigheden af den tidligere programudførsel det meget svært at forsta programmet, dels er dy-namisk scope i modstrid med ideen om modularitet af metoder, og endelig er dynamsik scopeineffektivt, idet vi først under selve programkørslen kan binde en forekomst af en identifier tilen given variabel.

Endelig skal nævnes levetiden af en variabel. Her opererer man med to typer, nemlig statiskeog automatiske variable.

En statisk variabel oprettes ved programstart og lever, indtil programmet afsluttes, mens enautomatisk variabel, f.eks. en lokal variabel i en procedure, kun eksisterer, salænge procedurener aktiveret. En vigtig pointe er, at hvis en procedure er aktiveret flere gange pa samme tid,f.eks. hvis vi har en rekursiv procedure, der har kaldt sig selv f.eks. 5 gange, sa eksistererden automatiske variabel i 5 uafhængige eksemplarer, mens der kun findes et eksemplar afden statiske variabel. Som vi skal se i i kapitel 10, allokeres automatiske variable normalthukommelsesplads i procedurens activation record, mens statiske variable er at finde i detsakaldte statiske hukommelsesomrade.

8.2 Implementering af symboltabellen

I selve symboltabellen gemmer vi alle relevante oplysninger om den enkelte identifier, somf.eks. type, placering i hukommelsen, en reference til den knude i AST’et, hvor identifierendeklareres, etc. Men hvilken datastruktur skal vi bruge til at gemme alle disse oplysninger i?

Symboltabellen skal understøtte følgende operationer:

• indsættelse af en ny identifier

• opslag: ud fra en identifiers navn søger vi forskellige oplysninger om identifieren

• opdatering af oplysningerne om en identifier

• sletning

• findes denne identifier allerede?

• findes denne identifier allerede i nuværende scope?

Ordet ”sletning”skal ikke tages helt bogstaveligt — vi vil ikke slette de møjsommeligtindsamlede oplysninger om vores identifier, men derimod blot markere, nar vi ikke længere eri identifierens scope.

Forskellen pa de to sidste operationer er følgende: Den næstsidste operation anvendes til atsikre, at en optrædende identifier faktisk er deklareret et eller andet sted. Den sidste operationanvendes ved deklarationer og bruges til at sikre, at den samme identifier ikke kan deklareresto gange i samme scope.

I litteraturen er der mange bud pa, hvilken underliggende datastruktur, man skal anvendei symboltabellen, men de fleste bruger en hashtabel. Det vil vi ogsa gøre.

For at løse problemet med de forskellige scopes og dermed forskellige symboltabeller, op-retter vi en symboltabel til hvert enkelt scope. Denne symboltabel har sa en reference tilsymboltabellen for det indesluttende scope, som vi kan søge i, hvis vi kommer ud for envariabel, der ikke findes i den umiddelbare symboltabel.

Et eksempel er vist pa sin plads. Betragt koden:

91

Page 92: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

int i, j, k; // scope A

void f() { // scope B

char j;

double i;

{ // scope C

char i;

...

}

}

void g() { // scope D

char k;

...

}

fgijk

void metodevoid metodeintintint

A

ij

doublechar

B

i char

C

k char

D

Figur 34

Her vil symboltabellerne fa udseendet som pa figur 34.

Nu til de gustne implementations-detaljer:Vi anvender java.util -klassen HashMap som den grundlæggende tabel. I en sadan Has-

hMap indsættes par af typen (key, value), hvor key er det objekt, vi søger pa, og value erdet objekt, der et tilknyttet key, og som vi gerne vil have fat i.

Vores key er naturligvis en Identifier , mens det er mere uklart, hvad value skal være:Vi skal i al almindelighed have et objekt, der kan indeholde alle relevante oplysninger, f.eks.type, adresse i hukommelsen etc....

Vi løser dette problem ved at lade value være af type Property , som er en klasse, vi selvdefinerer, og som afhænger stærkt af det konkrete problem. I næste sektion skal vi f.eks. huskepa værdien af en variabel, og her ville det være naturligt at lade Property indeholde en int:

public class Property {

public int value;

public Property(int value) {

this.value = value;

}

}

92

Page 93: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Vores Identifier skal have en passende hashCode tilknyttet sig — og det skal gælde, atens identifiers har ens hashkoder. Nu er det eneste, der identificerer en identifier netop densnavn, som er en String , sa vi tager simpelthen og implementerer metoden hashCode vedat returnere hashCode’n for strengen name. Metoden equals skal endvidere modificeres til atsammenligne name-attributten for to identifiers.

public class Identifier extends AST {

public String name;

public Identifier(String name) {

this.name = name;

}

public Object visit(Visitor v, Object obj) {

return v.visitIdentifier(this, obj);

}

public int hashCode() {

return name.hashCode();

}

public boolean equals(Object obj) {

return ((Identifier) obj).name.equals(name);

}

}

I selve klassen SymbolTable har vi to attributter, nemlig next, som er en reference tilsymboltabellen i det omsluttende scope, og table, som er selve vores HashMap.

Endvidere far vi følgende operationer:

• void insert(Identifier i, int v), som indsætter en ny værdi i table

• boolean doesExist(Identifier i), som returnerer true, hvis i faktisk eksisterer, ogellers false

• boolean doesExistInScope(Identifier i), som returnerer true, hvis i faktisk eksi-sterer i det nuværende scope, og ellers false

• int getValue(Identifier i), som returnerer Property -objektets værdi

• void setValue(Identifier i, int v), som ændrer i’s værdi til v

getValue og setValue er naturligvis meget specifikke for det konkrete problem i sektion 8.3— i andre sammenhænge ville man maske have metoder som setType eller getAdress.

Metoderne doesExist, getValue og setValue er alle rekursive: Findes identifieren ikke i detnuværende scope, sa skal next-tabellen gennemsøges rekursivt.

Koden for SymbolTable bliver:

import java.util.*;

public class SymbolTable {

private SymbolTable next;

private HashMap table;

public SymbolTable(SymbolTable next) {

this.next = next;

table = new HashMap();

93

Page 94: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

}

public void insert(Identifier i, int v) {

table.put(i, new Property(v));

}

public boolean doesExist(Identifier i) {

if (table.containsKey(i))

return true;

if (next == null)

return false;

return next.doesExist(i);

}

public boolean doesExistInScope(Identifier i) {

return table.containsKey(i);

}

public int getValue(Identifier i) {

if (table.containsKey(i))

return ((Property)table.get(i)).value;

if (next == null)

return 0;

return next.getValue(i);

}

public void setValue(Identifier i, int v) {

if (table.containsKey(i)) {

((Property)table.get(i)).value = v;

return;

}

else if (next != null)

next.setValue(i, v);

}

}

8.3 D+

Som et eksempel pa, hvorledes symboltabellen kan anvendes, skal vi implementere en fortolkerfor sproget D+ (D for deklaration, og ’+’, fordi den eneste aritmetiske operation er addition).

Et program i D+ er et aritmetisk udtryk, og antager som sadan en værdi. Det nye er, atvi inden i et udtryk kan deklarere variable.

Betragt f.eks. følgende D+-program: let x = 2 + 3, y = 3 + 4 in x + y end Imellem

let og in kan vi deklarere (og initialisere) en række variable, og deres værdi benyttes i detefterfølgende. Værdien af ovenstaende program er i øvrigt 12.

Disse deklarationer kan indlejres:

let x = 2, y = 3 in

let x = x+1, y =

( let z = 3 in x+y+z end)

in x+y

94

Page 95: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

end

end

I det yderste scope har vi variablerne x og y med værdierne 2 og 3. I den næste scope farvi en ny variabel x, som tildeles værdien 3 (den gamle værdi af x + 1), og en ny variabel y.Tildelingen af en værdi til y foregar i et nyt scope, hvor vi sætter z lig med 3 og beregnerx + y + z = 3 + 3 + 3 = 9. Endelig skal vi beregne værdien af x + y til sidst. Den senestdefinerede x har værdien 3 og den seneste y værdien 9, og summen bliver 12.

Generelt gælder, at vi ved en deklaration x = x + 2 først beregner højresiden, dernæstopretter den nye variabel, og endelig tildeler denne den beregnede værdi.

Den samme identifier ma ikke defineres to gange i samme scope — saledes er følgendeprogram ulovligt: let x = 2, x = 3 in x end Naturligvis er det ogsa ulovligt at anvende

ikke-definerede identifiers.

En grammatik for D+ er:

S → expexp → exp + term | termterm → (exp) | id | num | let decl-list in exp enddec-list → dec-list , decl — decldecl → id = exp

og den tilsvarende abstrakte grammatik bliver:

S → expexp → exp + exp | (exp) | id | num | let decl-list in exp enddec-list → dec-list , decl — decldecl → id = exp

JLex- og CUP-filerne skrives pa sædvanlig vis, og AST-klaserne implementeres ligeledessom før. Dog — for at kunne fortolke D+-udtrykkene, far hver enkelt knude i AST’et optil tre attributter: En boolean error, som fortæller, om der optræder semantiske fejl (ikke-deklarerede attributter etc.), en symtab som refererer til en symboltabel, og endelig en val aftypen int, som angiver udtrykkets værdi.

For at kunne implementere vores fortolker, som samtidigt tjekker, om en D+-program erkorrekt, skal vi skrive en attribut-grammatik.

Nontermialer af typen S, exp og decl-list tilknyttes tre attributter, nemlig error, symtabog val. id og num far kun en val, mens decl far en error og en symtab.

error og val er synthetiserede attributter, mens symtab er en nedarvet attribut.

95

Page 96: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

S → exp S.error = exp.errorS.symtab = exp.symtabS.val = exp.val

exp1 → exp2 + exp3 exp1.val = exp2.val + exp3.valexp1.error = exp2.error ∨ exp3.errorexp2.symtab = exp1.symtabexp3.symtab = exp1.symtab

exp1 → (exp2) exp1.val = exp2.valexp1.error = exp2.errorexp2.symtab = exp1.symtab

exp → id exp.val = exp.symtab.getValue(id)exp.error = ¬exp.symtab.doesExist(id)

exp → num exp.val = num.valexp.error = false

exp1 → let decl-list in exp2 end exp1.val = exp2.valexp1.error = decl-list.error ∨ exp2.errorexp2.symtab = dec-list.symtab

dec-list1 → dec-list2 , decl dec-list1.error = dec-list2.error ∨ decl.errordecl.symtab = dec-list2.symtabdec-list1.symtab = decl.symtab

dec-list → decl dec-list.error = decl.errordecl.symtab = dec-list.symtabdec-list.symtab = decl.symtab

decl → id = exp decl.error = decl.symtab.doesExistInScope(id) ∨ exp.errordecl.symtab.insert(id, exp.val)

Ud fra attribut-grammatikken er det nu en relativt simpel sag at implementere en visitor,der traverserer AST’et og beregner alle attributterne — men man bliver nok nødt til at tegneforskellige afhængighedsgrafer:

public class SymbolTableVisitor implements Visitor {

public Object visitIdentifier(Identifier node, Object obj){

return null;

}

public Object visitNumeral(Numeral node, Object obj){

node.val = node.value;

return null;

}

public Object visitBinOpExp(BinOpExp node, Object obj){

node.exp1.symtab = node.symtab;

node.exp2.symtab = node.symtab;

node.exp1.visit(this, obj);

node.exp2.visit(this, obj);

node.val = node.exp1.val + node.exp2.val;

node.error = node.exp1.error || node.exp2.error;

return null;

}

96

Page 97: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

public Object visitNumberExp(NumberExp node, Object obj){

node.value.visit(this,obj);

node.val = node.value.val;

node.error = false;

return null;

}

public Object visitIdExp(IdExp node, Object obj){

node.error = !node.symtab.doesExist(node.value);

if (!node.error)

node.val = node.symtab.getValue(node.value);

return null;

}

public Object visitDeclExp(DeclExp node, Object obj){

node.symtab = new SymbolTable(node.symtab);

node.declist.symtab = node.symtab;

node.exp.symtab = node.symtab;

node.declist.visit(this, obj);

node.exp.visit(this, obj);

node.val = node.exp.val;

node.error = node.exp.error || node.declist.error;

return null;

}

public Object visitDecList(DecList node, Object obj){

node.decl.symtab = node.symtab;

if (node.next != null) {

node.next.symtab = node.symtab;

node.next.visit(this, obj);

}

node.decl.visit(this, obj);

if (node.next != null)

node.error = node.next.error || node.decl.error;

else

node.error = node.decl.error;

return null;

}

public Object visitDecl(Decl node, Object obj){

node.exp.symtab = node.symtab;

node.exp.visit(this, obj);

node.error = node.exp.error || node.symtab.doesExistInScope(node.id);

node.symtab.insert(node.id, node.exp.val);

return null;

}

}

Selve visitoren kan testes ved følgende:

public class DplusCompiler{

97

Page 98: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

public DPlusCompiler(String filename) {

InputStream inp;

try {

inp=new FileInputStream(filename);

} catch (FileNotFoundException e) {

throw new Error("File not found: " + filename);

}

parser p = new parser(new Yylex(inp));

try {

p.parse();

} catch (Throwable e) {

e.printStackTrace();

throw new Error(e.toString());

} finally {

try {

inp.close();

} catch (IOException e) {}

}

SymbolTableVisitor stv = new SymbolTableVisitor();

p.parseResult.symtab = new SymbolTable(null);

p.parseResult.visit(stv, " ");

System.out.println(p.parseResult);

System.out.println(p.parseResult.error);

System.out.println(p.parseResult.val);

}

public static void main(String argv[]) {

String filename = "3.dplus";//argv[0];

new DPlusCompiler(filename);

}

}

Opgaver

Opgave 8.1 Tegn afhængighedsgraferne for attribut-grammatikken for D+, og kontroller im-plementeringen af klassen SymbolTableVisitor .

Opgave 8.2 Betragt følgende Java-program:

public class Scope {private int i = 2, j = 3, k = 4, m = 5;

public static void main(String[] args) {int i = 7, j = 9, m = 11;p(i, m)

}

98

Page 99: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

public static void p(int x, int i) {System.out.println("i = " + i);System.out.println("j = " + j);System.out.println("k = " + k);System.out.println("m = " + m);

}}

Hvad udskriver programmet? (Husk, at Java har statisk scope). Hvad ville programmetudskrive, hvis Java havde dynamisk scope?

99

Page 100: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

9 Typer og typecheck

Vi kommer nu til en af de sidste faser i den semantiske analyse, nemlig typechekket. Ideen erher at kontrollere, hvorvidt vort sprogs typeregler overholdes samtlige steder i AST’et, hvorder anvendes en identifier. I visse sprog skal vi ogsa indsætte typecasts, som er underforstaedei kildekoden, eller sagar finde ud af, hvilken type et udtryk skal tildeles.

I forbindelse med, at vi beskæftiger os med typecheck er det rimeligt at ga i gang med at sepa køretidsomgivelserne. Dette dækker over, hvorledes computerens hukommelse organiseresunder kørslen af programmet.

For de fleste platforme (processortyper og operativsystemer) er der en række regler ogkonventioner omkring køretidsomgivelserne, som compileren skal kende til og overholde. Forat gøre gennemgangen af syntese-delen af compileren lettere, er det en forel allerede nu at sepa, hvorledes køretidsomgivelserne fungerer for typiske systemer.

I dette kapitel skal vi se, hvorledes forskellige datatyper kan organiseret i hukommelsen,mens vi i senere kapitler skal se pa understøttelse af procedurer og pa automatisk hukommel-seshandtering (især garbage-collection).

9.1 Simple datatyper

Alle programmeringssprog tilbyder en række simple eller primitive datatyper — og disse ersom regel understøttet direkte af computerens hardware. Det er som regel tale om en elleranden form for heltal, for reelle tal, for karakterer og for boolske værdier.

I Java har man f.eks. hele 4 typer heltal — byte, short, int og long. Forskellen mellemdem ligger udelukkende i det talomrade, hvori værdierne kan ligge. Ideen er, at en byte skalfylde en byte, og derfor kan antage værdier mellem -128 og +127, mens en short fylder tobytes, en int 4 bytes og en long 8 bytes, med tilsvarende større talomrader. I praksis vilbade en byte, en short og en int fylde 4 bytes — dette gør det nemmere for Java’s virtuellemaskine.

Endvidere opererer man i Java med float og double. Disse datatyper repræsenterer reelletal, men hvor en float fylder 4 bytes, fylder en double 8 bytes. I begge tilfælde repræsentereset reelt tal vha. IEEE 754-standarden.

Java har ogsa en primitiv datatype char. Den repræsenterer en karakter efter Unicode ogfylder 4 bytes.

Endelig har Java datatypen boolean. Selvom en bit her kunne være nok, sa vælger manved JVM at lade en boolean fylde 4 bytes.

De fleste andre programmeringssprog tilbyder tilsvarende datatyper. En ting, som man dogskal passe pa, er meget tit at talomradet for f.eks. en int, er maskinafhængigt: I C er en intpa nogle platforme et 2-bytes tal, mens det pa andre er et 4-bytes tal. Tilsvarende er det ikkealtid standardiseret, om en char er en ASCII-karakter eller en Unicode-karakter.

Visse programmeringssprog, f.eks. Pascal, tillader en at definere subranges. Et eksempel erPascal deklarationen a : 1..10 ; som deklarerer en variabel, som kun kan antage værdiermellem 1 og 10. Dette er en stor bekvemmelighed, idet man kan undga en masse fejl: Underden semantiske analyse vil en Pascal-compiler f.eks. ikke acceptere udtrykket a := 132;

Det siger sig selv, at det kun er ordinale datatyper (dvs. datatyper, der kan opremses),som kan danne grundlag for subranges. I praksis er der tale om ints og andre heltalstyper, ogchar, som man kan subrange.

100

Page 101: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

9.2 Enumererede datatyper

Enumererede datatyper er en udbygning af subranges fra sidste sektion. Forskellen er, at mankan tildele de enkelte værdier et navn, hvorved det bliver meget lettere at vedligeholde koden.

F.eks. kan man i C deklarereenum Ugedag { mandag, tirsdag, onsdag, torsdag, fredag, lørdag, søndag };hvorved vi har en ny datatype Ugedag til radighed, der kun kan antage værdierne ’man-

dag’, ’tirsdag’ ..., ’søndag’.Compileren vil som regel lynhurtigt oversætte værdierne i en enumereret datatype til et

heltal — f.eks. ville man i ovenstaende opdage, at værdien ’mandag’ vil blive repræsenteretaf talværdien 0, ’tirsdag’ af 1, osv.

Java understøtter desværre ikke enumererede datatyper, men man kan simulere dem. Derfindes mindst to mader, hvoraf den mest almindelige er følgende:

public class Weekday {

public static final int monday = 0;

public static final int tuesday = 1;

public static final int wednesday = 2;

public static final int thursday = 3;

public static final int friday = 4;

public static final int saturday = 5;

public static final int sunday = 6;

}

Dette er dog ikke specielt typesikkert: Vi opererer stadigvæk med en int, og selvom etkorrekt udtryk ville være

int day = Weekday.saturday;

sa ligger der en evig fristelse til at skriveint day = 5;

Endnu værre er det, at man kan skrive noget i stil medint day = 37;

hvilket ikke kan fanges i den semantiske analyseLarman foreslar i [Lar] følgende typesikre variant:

public final class Weekday {

public static final Weekday monday = new Weekday();

public static final Weekday tuesday = new Weekday();

public static final Weekday wednesday = new Weekday();

public static final Weekday thursday = new Weekday();

public static final Weekday friday = new Weekday();

public static final Weekday saturday = new Weekday();

public static final Weekday sunday = new Weekday();

private Weekday() {

}

}

101

Page 102: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Dels far vi nu aktiveret Java’s typecheking, sa mange af de føromtalte uhensigtmæsighederog fejl undgas, men idet vi har en private constrcutor, sa kan man ikke udenfor klassenWeekday konstruere nye objekter.

Endvidere tillader ovenstaende scenario, at man tilføjer attributter og metoder til Week-day -objekterne, f.eks. nummeret pa dagen i ugen.

9.3 Arrays, records og pointere

Arrays forekommer i to typer: Statiske arrays og dynamiske arrays. Forskellen mellem disse totyper er, at man ved statiske arrays kender størrelsen af arrayet pa compileringstidspunktet— det gør man ikke ved dynamiske arrays.

Statiske arrays findes f.eks. i Pascal. Typisk array-deklarationer kunne her være:

var x: array[1..10] of integer;

var y: array[-3..8] of real;

var z: array[Weekday] of integer;

Som index kan man i Pascal benytte en vilkarlig ordinal datatype — i praksis som regelen subrange af int eller en enumereret datatype.

Dynamiske arrays findes i Java. Her skal man ikke i deklarationen angive arrayets størrelse— størrelsen fastlægges først pa kørselstidspunktet. Til gengæld indexeres alle arrays i Javafra 0 og opefter.

Lad os se lidt pa, hvorledes et array gemmes i hukommelsen. For at gøre tingene simplebetragter vi kun dynamiske arrays som i Java, og vi ignorerer flerdimensionale arrays, da disseikke findes i Java. Den interesserede læser henvises til [PZ] eller [WB] for yderligere detaljer.

I Java er et array faktisk et objekt, og som sadan kan man ikke tilga det direkte, men kunvia en reference. Saledes vil koden int[] p; kun deklarere en reference p til et array.

Selve arrayet bestar af et stykke hukommelse. Først kommer der en descriptor, som bl.a.fortæller, hvor stort arrayet er (dvs. den øvre grænse) og hvor stor det enkelte element iarrayet er, malt i f.eks. bytes. Dernæst kommer p[0], p[1], etc . . .

Vil man tilga cellen p[n], sa skal man beregne adressen &p + elemSize(p) ∗ n, hvor &p erdet sakaldte offset — defineret som adressen for p[0] — og elemSize(p) er størrelsen af denenkelte celle i arrayet.

Under compileringen — ja, faktisk under typechekket — vil compileren ved deklarationenaf arrayet finde elemSize(p) og placere dette tal i symboltabellen.

Records som sadan findes ikke i Java (omend objekter er at betragte som en udvidelse afrecord-begrebet), men til gengæld i f.eks. C og Pascal.

Betragt følgende C-deklaration:

struct {

double r;

int i;

}

Variable af denne datatype bestar af to komponenter — et reelt tal r og et helt tal i. Er xen sadan variabel, sa tilgas komponenterne ved x.r og x.i.

102

Page 103: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

I hukommelsen vil r og i ligge forløbende — i symboltabellen ligger informationernefieldOffset(x, r) = 0 og fieldOffset(x, i) = 8 (vi antager, at en double fylder 8 bytes ogen int 4 bytes) — dette betyder, at x.r findes pa adresse &x + fieldOffset(x, r) = &x og x.ipa &x+ fieldOffset(x, i) = &x+ 4.

Mere komplicerede former for records findes — f.eks. tillader Pascal sakaldte variant records.Igen henvises læseren til literaturen, f.eks. [PZ], [Seb] eller [WB].

Pointere er ganske nemme at implementere, idet en pointer jo bare er en adresse. Vi skalderfor ikke sige mere om disse.

9.4 Objekter

Endelig har vi objekter. Objekt-orienterede typer er en kompliceret historie, og vi vil kun herkort skitsere, hvorledes objekter implementeres i Java:

Hver enkelt objekt repræsenteres i hukommelsen som en record — det første felt er enreference til den sakaldte class descriptor, og de resterende felter er objektets attributter.

Class descriptoren indeholder generelle oplysninger om objektets klasse, superklasse ogeventuelle implementerede interfaces, samt en liste af referencer — en reference for hver me-tode i klassen. Referencen for metoden a() refererer til det stykke kode, der skal udføres, narmetoden a kaldes.

Betragt f.eks. følgende Java-klasser (hvor metodernes kroppe udelades)

public class Point {

protected int x, y;

public Point(int x, int y) { ... }

public void move(int dx, int dy) { ... }

public double area() { return 0.0; }

}

public class Circle extends Point {

protected int r;

public Circle(int x, int y, int r) { ... }

public int radius() { return r; }

public double area() { ... }

}

public class Box extends Point {

protected int w, h;

public Box(int x, int y, int w, int h) { ... }

public int width() { ... }

public int height() { ... }

public double area() { ... }

}

Betragt først følgende deklarationer:

Point p = new Point(3,4);

Point q = new Point(0,0);

103

Page 104: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Her oprettes der to referencer p og q til to nye Point-objekter. Disse to objekter pegerpa den samme class-descriptor. I descriptoren er der referencer til tre stumper kode — enreference for hver metode, incl. constructoren. Se figur 35.

class

3

4

class

0

0

p

q

Point

Point(-,-)

area()

mode(-,-)

kode:

kode forPoint-constructor

kode forPoint.mode

kode forPoint-area

Figur 35

Betragt nu koden:

Circle c = new Circle(2,3,4);

Box b = new Box (3,4,5,6);

Efter disse deklarationer far vi nu situationen pa figur 36.Ved at lade descriptoren bestemme, hvilket kode, der skal udføres ved et metodekald, opnar

vi at understøtte bade metoder, der overskrives i subklasser, og metoder, der ikke overskrives.

9.5 Assignments

Assignments er i virkeligheden ret subtile statements, og de fortjener en særegen behandling,idet semantikken af et assignment stærkt afhænger af typen af de indgaende variable.

Generelt anvender vi følgende notation: I assignmentet x = y beregner vi først r-værdienaf y, og denne værdi tildeler vi til l-værdien af x. (r-værdi star for ”righthand value”, ogtilsvarende l-værdi ”lefthand value”).

Betragt f.eks. tildelingen x = 4. Her finder vi først r-værdien af konstanten ’4’, dvs. tallet4. Dernæst finder vi l-værdien af x, dvs. det sted i hukommelsen, hvor x er placeret. Endeligtildeler vi, dvs. pa x’s sted i hukommelsen placerer vi tallet 4.

Normalt er der ikke de store problemer med at skelne mellem l- og r-værdier i assignments,undtagen nar der er tale om pointere og referencer. Betragt f.eks. Java-koden:

Point p = new Point(3,4);

Point q = new Point(0,0);

p = q;

104

Page 105: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

class

3

4

class

0

0

p

q

Point

Point(-,-)

area()

move(-,-)

kode:

kode forPoint-constructor

kode forPoint.move

kode forPoint.area

class

2

3

c

class

3

4

b

5

4

6

Circle

Circle(-,-,-)

area()

move(-,-)

Box

Box(-,-,-,-)

area()

move(-,-)

radius()

width()

height()

kode forCircle-constructor

kode forCircle.area

kode forCircle.radius

kode forBox-constructor

kode forBox.area

kode forBox.width

kode forBox.height

Figur 36

105

Page 106: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Husk, at i Java arbejder vi kun med objekt-referencer, sa her er forskellen mellem l- ogr-værdi særdeles relevant.

Sætningen p = q skal tolkes som følger: Først bestemmer vi r-værdien af q, og da q er enreference, sa er denne r-værdi lig med det objekt, som q peger pa, nemlig punktet (0,0).

Dernæst finder vi l-værdien af p, dvs. placeringen af p i hukommelsen. Da p er en reference,star der her en adresse, og før tildelingen star her adressen pa punktet (3,4). I selve tildelingenerstatter vi denne adresse med adressen pa punktet (0,0).

Resultatet er nu, at bade p og q peger pa det samme objekt, nemlig punktet (0,0). Ingenaf punkterne (0,0) eller (3,4) er blevet ændret pa nogen made under disse øvelser.

9.6 Typecheck

Vi kan nu endelig beskrive den sidste fase i den semantiske analyse, nemlig typechekket.Formalet med typechekket er ikke sa meget at generere mere information, som kan dekorere

AST’et (selvom dette ogsa forekommer), men derimod at kontrollere, om en række regler eroverholdt.

Disse regler afhænger fra sprog til sprog, men kunne være:

• enhver identifier skal deklareres, før den kan anvendes

• i enhver forekomst af en identifier skal identifieren anvendes korrekt i forhold til denstype — man kan f.eks. ikke multiplicere to strenge, eller tildele en int en record-værdi.

• i visse tilfælde skal en implicit typecast gøres explicit

Disse regler skal overholdes for at fjerne en lang række programmeringsfejl — eksempelviskan det nævnes, at JavaScript ikke kræver, at en variabel skal deklareres, før den anvendes,og i JavaScript optræder følgende fejltype ofte:

var enIdentifier = 7;

...

enidentifier = 9;

writeln(enIdentifier);

Her vil man fa det overraskende resultat 7 og ikke som forventet 9. Fejlen skyldes natur-ligvis, at JavaScript er case-sensitivt, og at der i den næstsidste linie er en slafejl: ”enidenti-fier”skulle være ”enIdentifier”.

Implicitte typecasts forekommer i mange sprog, f.eks. i Java. Betragt koden:

int i = 7;

double d = 4.5;

System.out.println(i+d);

Hvilken type har udtrykket i + d ? Ja, ifølge Java-specifikationen er typen double —int’en i typecastes implicit til en double, før additionen foregar.

Problemet er, at dette typecast pa kodeniveau ikke er automatisk: En int er et 4-bytes tali to-komplement-formatet, og en double fylder 8 bytes i IEEE-formatet, og det er absolutikke-trivielt at oversætte det ene til det andet.

106

Page 107: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Der skal derfor genereres noget kode, der udfører dette typecast, og i AST’et indikerer vidette ved at indsætte en ny knude i træet, som faktisk foretager dette typecast.

Pa kildekodeniveau svarer det til, at vi erstatter udtrykket i + d med (double) i + d.I praksis foretages typechekket samtidigt med udfyldelsen af symboltabellen: Hver gang vi

støder pa en deklaration, indsætter vi identifieren med dens type i symboltabellen, og hvergang en identifier optræder, kontroller vi, at typereglerne er overholdt.

107

Page 108: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

10 Procedurer

En af de væsentligste konstruktioner i moderne programmeringssprog er procedurer. Kærtbarn har mange navne: Procedurer, funktioner, metoder, subprogrammer, rutiner, etc..., mende fundamentale mekanismer er de samme.

Vi skal se pa, hvorledes parametrene i procedurekaldet overføres til proceduren, og hvorledesder kan sendes information tilbage ved procedurens afslutning. Det viser sig, at der findes i alt4 forskellige mader for parameteroverførsel — alle med deres specielle egenskaber og særheder.

Vi skal ogsa undersøge, hvorledes computerens hukommelse kan organiseres, saledes atinformation (især lokale variable) kan gemmes, mens der udføres et procedurekald. Detteforegar som regel ved at gemme sakaldte activation records i en stak.

Det viser sig, at der er en nær sammenhæng mellem ovenstaende og selve CPU’ens opbyg-ning, sa vi afslutter med en kort gennemgang af de to vigtigste former for CPU’ere, nemligRISC- og CISC-processorerne.

10.1 Hukommelsens organisering

I store træk kan computerens hukommelse opdeles i følgende 4 omrader:

• kode-omradet

• det statiske omrade

• stakken

• heapen

I selve kode-omradet gemmes den maskinkode, der skal udføres. Det vil som regel væretet stort, sammenhængende omrade, selvom mere sofistiskerede arkitekturer tillader dynamiskindlæsning og sletning af kodestumper. Men i praksis vil kode-omradet have en fast størrelse.

I det statiske omrade allokeres globale variable og statiske variable. Ligesom kode-omradethar dette omrade en størrelse, som er kendt pa compilerings-tidspunktet.

Pa stakken placeres activation records (mere herom senere). Disse placeres i en first-in-last-out rækkefølge. De fleste processorer indeholder registre og kommandoer, der tillader ennem og effektiv manipulation af stakken.

Af historiske arsager befinder stakkens top sig øverst i hukommelsen, og stakkens top paen hukommelsescelle med en lavere adresse. Sa stakken vokser altsa nedad og svinder opad— ligesom en istap.

Endelig har vi heapen. Dette er et hukommelsesomrade uden nogen fast struktur. I detteomrade allokeres heap-variable — variable og objekter med en størrelse, der ikke nødvendigviskendes pa compileringstidspunktet. Disse heap-variable kan, i modsætning til variablene istakkens activation records, leve ud over aktiverindstidsrummet for den procedure, hvori deblev skabt. Heap-variable kan kun tilgas via pointere eller referencer.

I Java vil alle static variable blive placeret i det statiske omrade, alle lokale variable afprimitive datatyper placeret i en activation record, mens alle objekter vil være at finde iheapen.

108

Page 109: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

kode-området

det statiske område

stakken

heapen

activation record

activation record

activation record

activation record

Figur 37

En activation record (AR) skabes, nar der kaldes en procedure. I AR’en gemmes værdi-erne af de parametre, der indgik i procedure-kaldet, her er allokeret plads til lokale variabelog til visse mellemregninger, og endelig indeholder AR’en en række informationer, som ernødvendige for at bogholderiet fungerer. Disse informationer er bl.a. returadressen og adres-sen pa AR’en for den procedure, der kaldte vores procedure.

Tillader vores programmeringssprog rekursion, sa kan en given procedure have flere for-skellige AR’er pa samme tid. Dette giver nogle komplikationer, som vi skal se nærmere pa omlidt.

10.2 Fuldt statiske køretidsomgivelser

I fuldt statiske køretidsomgivelser har vi ingen stak eller heap. Alle AR’er gemmes i det statiskeomrade i et eksemplar.

Hver enkelt AR indeholder et omrade til de parametre, proceduren kræver, en returadressesamt hukommelse til lokale variable og eventuelle mellemresultater.

Nar en procedure skal kaldes, sa udfylder den kaldende procedure parameter-omradet ogreturadressen og overlader scenen til den kaldte procedure.

Nar proceduren er færdig, skriver den en eventuel returværdi i en celle i AR’en, og pro-gramudførslen fortsættes pa returadressen.

Problemet med fuldt statiske køretidsomgivelser er, at hver enkelt procedure kun kan væreeksistere i en inkarnation ad gangen. Dette betyder, at f.eks. rekursive procedurer ikke er

109

Page 110: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

tilladt.Der er ingen moderne programmeringssprog, der har fuldt statiske køretidsomgivelser, men

noget ældre sprog havde ofte dette. Saledes kan nævnes FORTRAN77 og tidlige versioner afBASIC. Tiny kan ogsa nemt nøjes med fuldt statiske køretidsomgivelser, idet Tiny slet ikkeunderstøtter procedurer.

10.3 Stak-baserede køretidsomgivelser

Stak-baserede køretidsomgivelser er de mest almindelige — i hvert tilfælde for imperativesprog som C, Pascal, Java og C++. I denne sektion betragter vi kun sprog, hvori der ikkefindes lokale procedurer — sprog med lokale procedurer som f.eks. Pascal behandles i næstesektion.

Selve udformningen af AR’en afhænger af den konkrete arkitektur, men i almindelighedhar den enkelte AR et udseende som pa figur 38:

parametre

control link

returadresse

lokale variable

mellemregninger

frame pointer

stack pointer

Figur 38

En sadan AR — ogsa kaldet en stack frame — indeholder nederst (husk, at stakken vok-ser oppefra og nedefter) parametrene til proceduren. Hernæst kommer control link’et, som ivirkeligheden er værdien af den forrige AR’s frame pointer, og returadressen. Dernæst er derallokeret plads til proceduens lokale variable, og endelig er der afsat plads til mellemregninger.

I CPU’en er der normalt to specielle registre, nemlig frame pointer og stack pointer. Stackpointeren peger pa stakkens top, mens frame-pointer peger ind midt i AR’en.

Grunden til, at parametrene er placeret nederst i AR’en er, at de i virkeligheden hørertil den kaldende procedures AR: Før vi kalder vores procedure, beregner vi de nødvendigeparametre, og placerer dem øverst pa stakken. Sa kalder vi proceduren, placerer en ny AR pastakken, og som ved sort magi vil parametrene være nederst i den nye AR.

110

Page 111: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Lad os give et konkret eksempel. Betragt følgende kode:

public class Stack1 {

public static int x=1, y=2;

public static void main(String[] args) {

int z = 2;

int w = fact(2);

}

public static int fact(int n) {

if (n == 0)

return 1;

else {

int t = fact (n-1);

return n*t;

}

}

}

Her har vi, for eksemplets skyld, en række variable (x, y og z), som ikke bruges til noget.Selve programmet beregner rekursivt 2!

Til at starte med kalder operativsystemet main-metoden, denne kalder fact(2), som kalderfact(1), og som kalder fact(0), hvorefter alle metoder returnerer.

Lige efter kaldet af fact(0) ser stakken ud som pa figur 39.Typisk tilgar vi parametrene og de lokale variable vha. frame-pointeren. Faktisk kan vi

allerede nu tildele adresser til parametrene og de lokale variable: Antager vi, at hver enkeltadresse og variabel fylder 4 bytes, kan vi nemt finde forskellen mellem variablens adresse ogframe-pointeren (fp).

Betragter vi f.eks. en AR for metoden fact fra eksemplet — se figur 40 — ses det, atparametren n er at finde pa adressen fp + 4 og den lokale variabel pa adressen fp − 8.Parametren til det næste kald af fact vil normalt ikke tilgas vha. frame-pointeren, men derimodvha. stack-pointeren (sp).

Lad os opsummere, hvad der skal ske inden en procedure kaldes og efter at proceduren erafsluttet.

Inden en procedure kaldes:

1) Argumenterne (de konkrete parameter-værdier) beregnes og pushes pa stakken. Stack-pointeren ændres herved, sa den hele tiden peger pa det nederste element i stakken

2) Push frame-pointeren pa stakken som det nye control link

3) Ændr fp-registret, sa det peger pa toppen af stakken — det er nemmest af kopiereværdien af stack pointeren ind i fp-registret.

4) Push returadressen pa stakken

5) Hop til koden for proceduren

6) Som det første skal procedurens kode allokere plads og initialisere de lokale variable —herved ændres stack pointeren.

Nar proceduren forlades:

111

Page 112: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

2 (y)

1 (x)

args

control link

returadresse

2 (n)

2 (z)

? (w)

control link

returadresse

control link

returadresse

control link

returadresse

1 (n)

? (t)

? (t)

? (t)

0 (n)

stakken

det statiske område

AR for main

AR for fact(2)

AR for fact(0)

AR for fact(1)

Figur 39

1) Kopier fp ind i sp — herved kommer vi af de nu ubrugelige lokale variable og mellem-regninger

2) Kopier kontrol-linket ind i fp

3) Hop til returadressen

4) Pop argumenterne fra stakken

5) Push en evt. returværdi pa stakken

Visse platforme, f.eks. Javas Virtuelle Maskine, tilbyder faktisk at gøre alt ovenstaendefor en, hvilket er en stor behagelighed, men for de fleste platforme og CPU’ere skal alt gøresmanuelt.

Som vi senere skal se, sa kan man ofte komme af sted med at placere (nogle af) argumenterneog de lokale variable i registre, uden at stakken skal indblandes. Endvidere bør man sørge for,at sa meget som muligt at ovenstaende placeres i koden for den kaldte procedure — hervedkommer koden kun til at sta et sted i modsætning til at duplikere koden alle steder, hvorproceduren kaldes.

112

Page 113: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

2 (n)

control link

returadresse

parameter til næstekald af fact

? (t)

fp

Figur 40

10.4 Stak-baserede køretidsomgivelser med lokale procedurer

En komplikation optræder i sprog, hvori der tillades lokale procedurer, som f.eks. Pascal.Betragt nedenstaende Pascal-kode:

program RunTime;

procedure p;

var n:integer;

procedure q;

begin

(* a reference to n is now non-local non-global *)

end; (* q *)

procedure r( n:integer )

begin

q;

end; (* r *)

begin (* p *)

n := 1;

r(2);

end; (* p *)

begin (* main *)

p;

end.

Her indeholder proceduren p to lokale procedurer q og r. Scope for variablen n defineret ip er hele p’s krop samt kroppen for q (men ikke r, da der her er en parameter ved navn n,som overskygger for vores variabel n).

Det er vigtigt, at en lokal variabel skal have adgang til variablerne i et omgivende scope.En made at sikre dette pa er at indføre et sakaldt access link i procedurens AR. Dette accesslink refererer til AR’en for den omgivende procedure.

Saledes ser stakken for ovenstaende program, lige efter kaldet af q, som pa figur 41.Flere indlejrede procedurer inden i hinanden udgør intet problem: Vi lader altid access

linket referere den næstinderste procedures AR. Skal man have fat i en variabel, der er de-klareret f.eks. to niveauer fra vores lokale procedure, ma man følge procedurens access linktil den næsteinderste procedure, følge dennes access link, og na frem til den AR, hvori denønskede variabel er at finde.

113

Page 114: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

<no access link>

control link

returadresse

access link

2 (n)

1 (n)

control link

returadresse

returadresse

control link

access link

AR for main

AR for p

AR for q

AR for r

Figur 41

I praksis komplicerer lokale procedurer køretidsomgivelserne betydeligt, og det er derforglædeligt, at lokale procedurer ser ud til at være ved at ga af mode. I modsætning til f.eks.enumererede datatyper savner forfatteren absolut ikke lokale procedurer i Java!

10.5 Fuldt dynamiske køretidsomgivelser

I visse funktionelle sprog som f.eks. ML er de stak-baserede køretidsomgivelser ikke tilstræk-kelige. Grunden hertil er, at ML understøtter højere-ordens funktioner.

Betragt følgende ML-kode:

fun f(x) =

let fun g(y) = x+y

in g

end

val h = f(3)

val j = f(4)

val z = h(5)

val w = j(7)

og — for læsbarhedens skyld — den tilsvarende (ikke-gyldige) C-kode:

int (*)() f(int x) {

int g(int y) {return x+y;}

return g;

114

Page 115: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

}

int (*h)() = f(3);

int (*j)() = f(4);

int z = h(5);

int w = j(7)

Ideen er her, at f skal returnere en ny funktion, g. En spidsfindighed er, at den returneredefunktion g afhænger af værdien af den lokale variabel x i f pa oprettelsestidspunktet.

Saledes vil værdierne af z og w i det ovenstaende kode være lig

z = x+ y = 3 + 5 = 8 og w = x+ y = 4 + 7 = 11

Dette ville ikke virke, hvis vi anvendte stak-baserede køretidsomgivelser, idet AR’en for f kunville leve, sa længe kaldet til f forløber, og AR’en er i hvert tilfælde ikke i live ved beregningenaf f.eks. z.

Løsningen er at anvende de sakaldte fuldt dynamiske køretidsomgivelser. Her anvender viikke stakken, men derimod heapen til at gemme procedurernes activation records. I lighedmed andre objekter pa heapen slettes et objekt ikke, med mindre der ikke længere er brugfor det (garbage collection), og da der refereres til de to AR’er for f , nemlig fra AR’erne forg-erne, fortsætter disse AR’s med at eksistere ud over kaldene til f .

Principielt fungerer fuldt dynamiske køretidsomgivelser pa nogenlunde samme made somde stak-baserede, men da CPU’en normalt ikke understøtter vilkarlige operationer i heapenpa samme made som stak-operationer, er fuldt dynamiske køretidsomgivelser ikke nær saeffektive som de stak-baserede. Men gevinsten er altsa en meget større fleksibilitet.

Den interesserede læser henvise til litteraturen for yderligere oplysninger, f.eks. til [App].

10.6 Parameteroverførselsmekanismer

En problemstilling, som er helt uafhænig af, hvorledes vi organiserer køretidsomgivelserne, erhvorledes parametrene fortolkes. Der findes i alt 4 forskellige mader, hvorpa denne tolkningkan forega, og vi skal nu se kort pa disse parameteroverførselsmekanismer.

De 4 mekanismer er:

• pass by value

• pass by reference

• pass by value-result

• pass by name

hvor vi som sædvanlig foretrækker at anvende de engelske betegnelser fremfor nogle mereeller mindre hjemmestikkede danske.

Den mest velkendte mekanisme er pass by value. Her er det r-værdien af parametren, deroverføres, dvs. vi kopierer værdien af parametren over pa stakken som argument.

I sprog som Java og C er pass by value den eneste mulige mekanisme, mens C++ og Pascalhar denne mekanisme som default.

I praksis tillader pass by value os at behandle den formelle parametre som nye variabler —de kan saledes ændre værdi, uden at det gar ud over den oprindelige variabel, vi brugte somparameter.

Saledes vil følgende C-procedure:

115

Page 116: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

void increment2(int x) {

x++;

x++;

}

ikke gøre noget som helst — ganske vist ændrer vi værdien af parametren x, men denneer jo en kopi af den originale parameter-variabel, og denne kopi smides væk, nar procedurenforlades.

I pass by reference overfører vi ikke parametrens r-værdi, men derimod l-værdien. Dettebetyder, at den kaldte procedure faktisk kender til parametervariablens adresse, og kan ga indog modificere denne. Ovenstaende metode increment2 ville virke glimrende efter hensigten,hvis x blev overført ved pass by reference.

I C og C++ kan pass by reference simuleres ved brug af pointere:

void increment2(int* x) {

(*x)++;

(*x)++;

}

men C++ tillader direkte pass by reference ved at anvende &-symbolet i parameterlisten:

void increment(int &x) {

x++;

x++;

}

I Pascal kan man tilsvarende fa pass by reference ved at anvende det reserverede ord var

i parameter-listen.Som før nævnt tillader Java kun pass by value, men i virkeligheden kommer man længst ved

at forestille sig, at alle objekter (men ikke primitive datatyper) overføres ved pass by reference.Dette skyldes, at i Java arbejder vi ikke direkte med objekter, men kun med referencer tilobjekter, og det er disse referencer, dvs. adresserne pa objekterne, vi overfører.

En mere sjælden mekanisme er pass by value-result. Denne mekanisme findes og anvendesisær i Ada. Ideen er, at parameterens r-værdi kopieres som ved pass by value, men efter endprocedure-udførsel kopieres denne værdi tilbage i den oprindelige variabel.

Pass by value-result kan give overraskende resultater — betragt f.eks. nedenstaende kode,hvor vi forestiller os, at parametrene overføres ved pass by value-result:

void p(int x, int y) {

x++;

y++;

}

main() {

int a = 1;

p(a, a);

}

116

Page 117: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Efter kaldet af p indeholder variablen a værdien 2 — vi skaber ved metodekaldet to kopieraf a, kaldet x og y. Begge har værdien 1, men incrementeres til 2. Ved metodekaldets afslutningkopieres x’s værdi 2 over i a, hvorefter y’s værdi 2 kopieres oveni a igen.

Ved pass by value ville a have den uændrede værdi 1, mens a ville have værdien 3 ved passby reference.

Endelig har vi den komplicerede pass by name. Denne mekanisme blev indført i Algol60,men viste sig svær at forsta og blev derfor ganske upopulær, og den forekommer næppe ipraksis nu om dage.

Ideen er, at argumentet ikke beregnes, førend vi faktisk nar til det sted i koden, hvor argu-mentet optræder — i praksis svarer det til en tekstuel substitution af den formelle parametermed den aktuelle parameter, heraf navnet.

Lad os betragte et eksempel:

int i;

int a[10];

void p(int x) {

i++;

x++;

}

main() {

i = 1;

a[1] = 1;

a[2] = 2;

p(a[i]);

}

Overføres parametren i kaldet af p ved pass by name, sker der følgende:I første linie af p incrementeres til værdien 2. I anden linie udfører vi, hvad der svarer til

erstatte x med a[i] — da i har værdien 2, incrementeres a[2] til 3, mens a[1] lades uændret.Ikke nok med, at pass by name er svær at forsta og ikke altid giver det forventede resultat

— det er ogsa temmeligt kompliceret at implementere pass by name i køretidsomgivelserne.Sa med rette er pass by name meget sjældent anvendt.

10.7 RISC- og CISC-processorer

I forbindelse med beskrivelsen af køretidsomgivelserne, især af understøttelsen af procedurerog stak-baserede køretidsomgivelser, kommer vi ikke uden om at beskrive ideen bag de sakaldteRISC-processorer og forskellen pa CISC- og RISC-processorer.

Beskrivelsen her er ganske overfladisk, og læseren henvises til f.eks. [Sta] eller [Tan] foryderligere oplysninger. Vi vil endvidere komme tilbage til emnet i kapitel 13.

Designet af køretidsomgivelserne, som beskrevet i sektion 10.3 og 10.4, var det gængse indtilmidten af 1980’erne. Alle lokale variable og parametre havde deres faste plads i hukommelsen,og hver gang, de skulle anvendes i beregninger, blev de hentet ind i CPU’ens registre, beregnetpa, og sendt tilbage til hukommelsen. Dette gav naturligvis en masse trafik mellem CPU’enog hukommelsen, og da det er op til 50 gange langsommere for CPU’en at kommunikere

117

Page 118: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

med hukommelsen end med sine registre, var dette ret ineffektivt. Desværre havde CPU’ernedengang ikke sa mange registre, sa det var svært — ja, ofte umuligt — at holde de lokalevariable i hvert sit register.

De dengang anvendte processorer var alle af CISC-typen — Complex Instruction Set Com-puter. De var karakteriseret ved at have relativt fa registre — en CISC-processor som 8086(Pentiums forgænger) havde 6 registre, som kunne bruges af programmøren. Til gengældhavde de store, komplekse instruktioner, som f.eks. kunne kopiere store dele af hukommelsenfra et sted til et andet med kun en instruktion.

Forskning omkring 1985 viste, at dette setup var temmeligt ineffektivt:

1) der blev brugt temmeligt meget tid pa at CPU’en kommunikerede med hukommelsen

2) de komplekse CISC-instruktioner var ikke specielt hurtigere end kodestumper sammen-sat af tilsvarende simple instruktioner — besparelsen la udelukkende i størrelsen afmaskinkoden

3) det var meget svært at fa compileren til at genkende situationer, hvor de komplekseCISC-instruktioner kunne anvendes, og i praksis blev disse komplekse instruktionerikke brugt

4) de fleste procedurer havde op til kun 4 parametre, og det var meget sjælden, at enprocedure havde mere end 6 parametre

5) de fleste procedurekald var til procedurer, der ikke kaldte andre procedurer igen

Man drog da følgende konklusioner: For det første kunne det ikke betale sig at have kom-plekse instruktioner. For det andet var det bedre at bruge CPU’ens kapacitet og transistorerpa at have mange registre end at have et omfattende stykke hardware, der kunne eksekveredisse komplekse instruktioner.

Man konstruerede derfor RISC-processorerne — Reduced Instruction Set Computer. DisseRISC-processorer er karakteriseret ved at have ganske simple instruktioner, men til gengældmange registre.

Ved at anvende en RISC-processor opnar man en lang række fordele. Nogle af disse er:

• det bliver muligt at placere stort set alle parametre og lokale variable i registre

• en hel del trafik mellem CPU’en og hukommelsen undgas

• for procedurer, der ikke kalder andre procedurer, behøver vi ikke altid at have en acti-vation record. Husk, at en af AR’s opgaver er at huske pa værdien af lokale variable,mens vi laver et procedurekald inden i vores procedure

• idet instruktionerne er relativt simple, bliver det meget nemmere at finde den rækkefølgeaf instruktioner, som giver den bedste (hurtigste) maskinkode

Vi skal senere, i kapitel 13 om kodegenerering og i kapitel 14 om registerallokering se pa,hvorledes dette foregar i praksis.

118

Page 119: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Opgaver

Opgave 10.1 Tegn, for nedenstaende C-program, stakken

a) netop efter at vi har kaldt metoden f for første gangb) netop efter at vi har kaldt metoden g for anden gang

Stakken organiseres som i sektion 10.3

int x = 2;void g(int); /* prototype */void f(int n) {static int x = 1;g(n);x--;

}void g(int m) {int y = m-1;if (y>0) {

f(y);x--;g(y);

}}main() {g(x);

}

Opgave 10.2 Betragt nedenstaende Pascal-program. Tegn stakken efter det andet kald afproceduren c, og beskriv, hvorledes variablen x tilgas inde fra c.

program opgave10_2;procedure a;

var x: integer;procedure b;

procedure c;begin

x := 2;b;

end;begin (* b *)

c;end;

begin (* a *)b;

end;begin (* main *)a;

end.

Opgave 10.3 Angiv outputtet af nedenstaende C-program for hver af de 4 parameterover-førselsmekanismer.

119

Page 120: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

int i = 0;void p(int x, int y ) {x++;i++;y++;

}main() {int a[2] = (1,1);p( p[i], a[i] );printf("%d %d\n", a[0], a[1]); // simpel udskrivning

}

Opgave 10.4 Angiv outputtet af nedenstaende C-program for hver af de 4 parameterover-førselsmekanismer.

int i = 0;void swap(int x, int y) {x = x+y;y = x-y;x = x-y;

}main() {int a[3] = (1,2,0);swap(i, a[i]);printf("%d %d %d %d\n"), i, a[0], a[1], a[2]);

}

Opgave 10.5 Betragt nedenstaende C-procedure:

void f(char c, char s[10], double r) {int *x;int y[5];...

}

Vi antager, at de to arrays s og y placeres pa stakken (dette er tilladeligt, idet vi kenderderes størrelse pa compileringstidspunktet). Vi antager endvidere, at en int fylder 2 bytes, enchar 1 byte, en double 8 bytes, en adresse 4 bytes, og at vi anvender C’s standard parame-teroverførselsmekanismer.

a) Tegn AR’en for ovenstaende procedure, og bestem offset i forhold til fp for c, s[7] ogy[2].

b) Samme spørgsmal som a), men hvor vi antager, at alle parametre overføres ved pass byvalue.

c) Samme spørgsmal som a), men hvor vi antager, at alle parametre overføres ved pass byreference

120

Page 121: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

11 Mellemrepræsentationen

Vi skal nu endelig i gang med syntese-delen af compileren, dvs. vi skal se pa, hvorledes mankan generere kode ud fra AST’et.

Det første skridt her er at oversætte AST’et, som er en abstrakt beskrivelse pa et højt niveauaf, hvad programmet skal kunne gøre, til mellemrepræsentationen, som er en væsentligt merekonkret beskrivelse pa et væsentligt lavere niveau.

Vi vil arbejde med three-address-code (3AC), som er en form for abstrakt maskinkode.Andre valg er mulige — [Lou] anvender udover 3AC ogsa P-kode (som minder en del omJVM’s Oolong), og i [App] er mellemrepræsentationen en variant af AST’et, men hvor vi idette træ har knuder, som indeholder information pa et lavere niveau, og som regel svarer tilen enkelt CPU’instruktion.

Mellemrepræsentation vil ofte blive forkortet IR (Intermediate Representation).

11.1 Begrundelse for mellemrepræsentationen

Hvorfor i det hele taget have en mellemrepræsentation? Kunne man ikke bare direkte oversættetil den endelige maskinkode, og sa arbejde videre med denne? Jo, det kunne man sagtens,men der er alligevel gode grunde til at have en mellemrepræsentation med:

Java

ML

Pascal

C

C++

Sparc

MIPS

Pentium

Alpha

Java

ML

Pascal

C

C++

Sparc

MIPS

Pentium

Alpha

IR

Figur 42

For det første isolerer mellemrepræsentationen compilerens front-end fra compilerens back-end. Husk, at front-end dækker over compilerens analysedel, hvori vi fortager den leksikalske,den syntaktiske og den semantiske analyse — og at alle tre faser er meget sprogafhængige,men platformsuafhængige. Back-end dækker derimod over kodegenerering og -optimering, ogdisse faser er meget platformsafhængige, men til gengæld sproguafhængige.

Lad os f.eks. forestille os, at vi skal udvikle en Java-, en ML-, en Pascal-, en C- og enC++-compiler, og at vi skal kunne producere kode til Sparc, til MIPS, til Pentium og til

121

Page 122: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Alpha — vi skal altsa i alt producere 20 compilere.Vælger vi at generere maskinkode uden en IR, sa kommer vi til at skrive 20 forskellige com-

pilere, idet vi hele tiden skal se pa, hvorledes vores konkrete programmeringssprog interfererermed vores konkrete platform — vi har hele tiden en ny situation. Se figur 42 til venstre.

Anvender vi derimod en fælles IR, sa skal vi kun skrive 5 front-ends, der kan oversætte etsprog til IR, og 4 back-ends — en væsentlig arbejdsbesparelse, som pa 42 til højre.

En anden fordel ved IR er, at der findes mange færdige algoritmer til at optimere og trans-formere IR — se f.eks. [Muc] — det gør der nødvendigvis ikke til den konkrete maskinkode,man i sidste ende vil ende op med.

Endelig er IR en nødvendighed, idet der simpelthen er alt for stor forskel mellem AST’etfor et typisk højniveausprog og hvad en CPU faktisk kan præstere. Nogle eksempler:

Alle højniveausprog har avancerede iterations- og selektions-mekanismer, sa som for- ogwhile-løkker, if-, if-else- og switch-strukturer. Pa CPU-niveau er det eneste, vi kan afdenne slags, at lave ubetingede eller betingede hop.

Som kapitel 9 og 10 viste, foregar der en hel masse bag scenen, nar man f.eks. tilgar etelement i et array eller kalder en procedure. Men da næsten ingen CPU’ere understøtter dettedirekte, sa skal man selv skrive kode til at kunne gøre dette.

Vi vil i dette kapitel arbejde med three address-code (3AC), som er en form for abstraktmaskinkode. Det er en er de mest udbredte former for IR, og læseren kan finde meget mereom 3AC i [Muc], [ASU] og [Lou].

11.2 Instruktioner i 3AC

Lad os starte med at give en liste over, hvilke ’instruktioner’ der findes i 3AC, og sa bageftergennemga typiske situationer.

Navnet three address code stammer fra det faktum, at de fleste instruktioner i 3AC er afformen x := y op z, hvor x, y eller z enten er variable eller compiler-genererede temporærevariable, og op er en binær operator som addition eller subtraktion. Betydningen af udtrykketer følgende: Tag indholdet af variablerne y og z, anvend op pa disse to tal, og gem resultateti variabel x.

En fuldstændig liste over instruktionerne er følgende:

1) Assignments af formen x := y op z, hvor x er en variabel eller temporær variabel, ogy og z er enten variable, temporære variable eller konstanter. op kan være addition,subtraktion, multiplikation, division, eller logiske operationer som OR, AND eller XOR.

2) Assignments af formen x := op y, hvor x er en variabel eller temporær variabel, og yen variabel, temporær variabel eller konstant. op kan være unær minus, logisk negationeller en shift-operation.

3) Kopi-instruktioner af formen x := y.

4) Et ubetinget hop — jump L — hvor udførslen fortsætter ved instruktionen med labelL.

5) Betingede hop af typen if a < b goto L. Mulige betingelser er relationelle sammen-ligninger (=, �=, <,≥, <,≤) mellem to variable eller temporære variable. Hvis sammen-ligningen er sand, sa hopper vi til instruktionen med label L, ellers fortsætter vi bareudførslen.

122

Page 123: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

6) Instruktioner relateret til metode-kald: param x fortæller, at variablen, den temporærevariabel eller konstanten x er parameter til et næste metodekald. call p kalder proce-duren med navnet p, og return y returnerer fra en procedure med resultatet y.

7) Indekserede assignments af formen x := y[i] og x[i] := y. Den første instruktiontildeler x værdien af hukommelsescellen pa adresse y + i, mens den sidste placererværdien y i hukommelsescellen med adresse x+ i.

8) Adresserings- og pointer-instruktioner af typen x := &y, x := *y og *x := y. Instruk-tionen x := &y finder adressen af variablen y og tildeler denne adresse-værdi til x. Iinstruktionen x := *y tildeles x indholdet af den hukommelsescelle, som har adresseny. I instruktionen *x := y kopieres indholdet af hukommelsescellen med adresse x overi variablen y.

En vigtig del af 3AC er de sakaldte temporære variable. Det er variable, der genereresautomatisk af compileren og indeholder som regel resultater af mellemregninger. VIi vil i detfølgende betegne de temporære variable som T1, T2 etc. I de resterende faser af compileringenvil vi ikke skelne mellem ’rigtige’ variable og temporære variable — nar de temporære variablegenereres, indsættes de i symboltabellen ligesom de almindelige variable.

Som vi skal se, genereres 3AC ved endnu en traversering af AST’et, og som sadan kan3AC-genereringen beskrives ved hjælp af attribut-grammatikker.

11.3 Udtryk i 3AC

Aritmetiske og logiske udtryk er nok de sværeste at skrive 3AC for. Men da vi kun far brugfor instruktioner af typerne 1), 2) eller 3) ovenfra, sa starter vi alligevel med dette.

Ideen er, at vi traverserer AST’et i post-order. Hver enkelt efterfølger til en knude generereren ny temporær variabel samt en enkelt 3AC-instruktion, og nar vi har genereret efterfølgernes3AC-instruktioner, er det en smal sag at genere 3AC-instruktionen for den konkrete knude.

Betragt udtrykket 2 ∗ a+ (b− 3). Dette har AST’et pa figur 43.

+

* -

2 a b 3

Figur 43

Traverserer vi dette træ i post-order, vil vi besøge knuderne i følgende rækkefølge: 2, a, ∗, b, 3,−,+.3AC-instruktionerne vil blive genereret efter følgende regler: Et blad med værdien v genere-

rer ingen instruktion, men returnerer værdien af bladet, mens en indre knude med operationenop og bladene Tx og Ty genererer instruktionen Tz = Tx op Ty og returnerer Tz.

For ovenstaende træ far vi derfor instrukionerne:

123

Page 124: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

2: -

a: -

* T2 := 2 * a

b: -

3: -

−: T2 := b - 3

+ T3 := T1+T2

Lad os opskrive en attribut-grammatik for en simpel aritmetik for aritmetiske udtryk. Selvegrammatikken er

exp→ exp+ exp | exp− exp | exp ∗ exp | − exp | id | numVi far brug for en enkelt attribut, temp, som indeholder en variabel eller konstant, samt enrække semantiske handlinger, nemlig write, som genererer en ny instruktion, og getTemp, somreturnerer en ny temporær variabel.

exp1 → exp2 + exp3 exp1.temp =getTemp()write( exp1.temp + ":="+ exp2.temp + "+ "+ exp3.temp)

exp1 → exp2 − exp3 exp1.temp =getTemp()write( exp1.temp + ":="+ exp2.temp + - "+ exp3.temp)

exp1 → exp2 ∗ exp3 exp1.temp =getTemp()write( exp1.temp + ":="+ exp2.temp + "* "+ exp3.temp)

exp1 → −exp2 exp1.temp = getTemp()

write( exp1.temp + ":="+ - "+ exp2.temp)exp→ id exp.temp =new Identifier(id)

exp→ num num.temp =new Numeral(num)

11.4 Kontrolstrukturer og procedurekald i 3AC

Dette foregar smertefrit — ideen er at forestille sig, hvorledes den givne konstruktion kunneimplementeres kun med ubetingede og betingede hop. Vi skal dog have en ny metode, der kangenerere og associere en label til en 3AC-instruktion; lad os kalde denne metode for getLabel.

Eksempelvis genererer konstruktionen if ( cond ) then stmt-seq 1 else stmt-seq 2

følgende 3AC-instruktioner:

if cond goto L1

(kode for stmt-seq1)goto L2

L1 : (kode for stmt-seq2)L2: (næste 3AC-instruktion)

Eller konstruktionen while ( cond ) do stmt-seq giver:L1 : if not cond goto L2

(kode for stmt-seq)L2 : (næste 3AC-instruktion)

Det er lidt bøvlet at skrive en attribut-grammatik for ovenstaende, men særdeles nemt atskrive visitor-metoder, der genererer 3AC-kode.

124

Page 125: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

11.5 Datatyper i 3AC

Elementer i arrays tilgas nemt og elegant ved at anvende indekserede adresseringer i 3AC.Uheldigvis er disse operationer pa et alt for højt niveau i forhold til de fleste processorer, ogman bør derfor omskrive dem, saledes at adresse-beregningerne gøres eksplicitte.

For simpelheds skyld betragter vi kun arrays, hvor det første index er 0. For arrayet akender vi derfor startadressen &a og — f.eks. i symboltabellen — kan vi finde størrelsen afdet enkelte element i a, elemSize(a).

Vil vi derfor udføre tildelingen x = a[y], bliver dette i 3AC til

T1 := y * elemSize(a)

T2 := &a + T1

x := *T2

og tilsvarende, a[x] = y oversættes til

T1 := x * elemSize(a)

T2 := &a + T1

*T2 := y

Lad os afslutte med et mere komplekst eksempel: a[i+1] = a[j*2] + 3. Som altid vedassignments beregner vi først højresiden, og dernæst adressen for venstresiden:

T1 := j * 2

T2 := T1 + elemSize(a)

T3 := &a + T2

T4 := *T3

T5 := T4 + 3

T6 := i + 1

T7 := T6 * elemSize(a)

T8 := &a + T7

*T8 := T5

Records er heller ikke mere komplicerede — for at finde adressen for x.j , hvor x er enrecord med et felt j, slar vi op i symboltabellen og finder offset’et fieldOffset(x, j).

F.eks. betragt kommandoen x.j = x.i . I 3AC finder vi først adressen og dernæst værdienfor højresiden, og dernæst adressen for venstresiden:

T1 := &x + fieldOffset(x,i)

T2 := *T1

T3 := &x + fieldOffset(x,j)

*T3 := T2

Simple pointer-statements oversættes nemt til 3AC: Betragt C-tildelingerne *x = i og i

= *x, hvor i er en int og x en *int. I 3AC oversættes disse trivielt til *x := i og i := *x.Antag, at vi har et typisk binært træ, hvori hver knude repræsenteres ved en TreeNode:

125

Page 126: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

typedef struct TreeNode {

int val;

struct TreeNode *lchild, *rchild;

} Treenode;

Hvis p nu er af typen *TreeNode, sa vil C-tildelingen p->lchild = p; blive oversat tilfølgende:

T1 := p + fieldOffset(*p, lchild)

*T1 := p

mens p = p->lchild; bliver til

T1 := p + fieldOffset(*p, rchild)

p := *T2

11.6 Implementering af 3AC

Et ikke uvæsentligt spørgsmal er, hvorledes vi implementerer 3AC — det er nok ikke heltfornuftigt at generere en tekstfil basis for den videre compilering.

I litteraturen beskrives forskellige alternativer — vi vil holde os til en af de simpleste,nemlig quadrupel-notationen.

I quadrupel-notationen repræsenterer vi hvert enkelt 3AC-instruktion som et 4-tupel afformen (i, x, y, z), hvor i indikerer instruktionens type, x er adressen pa venstre-siden og y ogz er adresserne pa højresiden. Ved mange instruktioner har vi ikke brug for alle 3 adresser,og der kan vi have en null-værdi pa passende steder.

Repræsentationen af de forskellige typer instruktioner bliver derfor:x := y op z (op , x, y, z)x := op y (op , x, y, −)x := y (copy , x, y, −)jump L (jump , L, −, −)if a > b goto L (ifGT , a, b, L)param x (param , x, −, −)call p (call , p, −, −)return y (return , y, −, −)x := y[i] (copyind, x, y, i)x[i] := y (indcopy, x, i, y)x := &y (copy , x, &y, −)x := *y (copy , x, ∗y, −)*x := y (copy , ∗x, y, −)En implementering kunne være noget i stil med:

public class ThreeAddressCode {

public static final int add = 0;

public static final int sub = 1;

...

public static final int copy = 27;

126

Page 127: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

public static final int jump = 28;

...

public int instruction;

public Operand x;

public Operand y;

public Operand z;

}

hvor de tre sidste elementer i quadruplen repræsenteres ved objekter af typen IdentifierellerNumeral fra kapitel 6. For at fa tingene til at passe, bør begge disse klasser implementereet interface Operand . Nu kan der jo optræde bade temporære variable, labels, *’er og &’eri disse felter. Det foreslas, at man specialiserer klassen ¡classIdentifier til tre subklasser, dertager sig af henholdsvis &- og *-operatorerne og labels, mens temporære variable oprettes ogindsættes i symboltabellen som almindelige identifiers, men med et navn, som umuligt kanforekomme som et gyldigt identifier-navn — de fleste compilere bruger traditionelt navne som$1, $2, $3 osv.

Opgave 11.1 Angiv 3AC-instruktioner svarende til nedenstaende udtryk:

a) 2+3+4+5

b) 2+(3+(4+(5)))

c) a*b+a*b*c

d) (x = y = 2) + 3*(z=4)

e) a[a[i]] = b[i=2]

f) p->next->next = p-> next

Opgave 11.2 Oversæt nedenstaende Tiny-program til 3AC:

{calculates the gdc of u and vread u;read v;if v = 0 then v : =0 {do nothing}elserepeat

temp := v;v := u - u/v*v; {computes u \% v}u := temp;

until v = 0end;write u

127

Page 128: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

12 Kodegenerering

Vi skal nu se pa, hvorledes vi oversætter fra mellemrepræsentationen til den endelige malkode.Detaljerne i denne oversættelse afhænger naturligvis stærkt af, til hvilken CPU man vil

skrive kode, og vi vil behandle bade RISC- og CISC-processorer. Kodegenerering til Java’sVirtuelle Maskine er noget speciel — det viser sig, at det ikke er sa smart at bruge 3AC sommellemrepræsentation, men meget nemmere direkte at oversætte fra AST’et.

En vigtig del af malkoden vil ikke blive berørt i dette kapitel, nemlig registerallokeringen.Dette vigtige skridt tager vi i kapitel 13.

Generelt er det ikke helt korrekt at sige, at vi genererer maskinkode — det gør professionellecompilere naturligvis; men i en compiler skrevet til undervisningsbrug er det glimrende, hvisman kan fa genereret assembler-kode, som en assembler (som f.eks. Oolong) sa kan oversættetil rigtig maskinkode.

I det følgende vil vi ikke anvende den korrekte assembler-notation for de forskellige pro-cessorer, vi genererer kode til, men anvende en form for 3AC-lignende kode. Dette er for atundga at dykke ned i de specielle tekniske detaljer omkring hver processor.

12.1 RISC-processorer

I en typisk RISC-processor er oversættelsen fra 3AC til assembler-kode ganske simpel: Prin-cippet er, at hver enkelt 3AC-instruktion umiddelbart svarer til en RISC-instruktion, omendder forekommer undtagelser.

RISC-processoren indeholder som regel 32 registre — nummereret R0 til R31 — og der eri princippet ikke den store forskel mellem dem:

R0 er ofte meget specielt, idet det er ”kortsluttet”: Det indeholder altid værdien 0, ogforsøger man at skrive en ny værdi ind i det, sa ignoreres denne værdi.

Endvidere kan der være nogle konventioner, man bør følge, omkring registrenes brug. F.eks.bør man i en UltraSPARC anvende R1–R7 til globale variable, R8–R15 til udgaende para-metre, R14 som stack pointer (som jo i virkeligheden er en form for udgaende parameter,idet stack pointeren jo bliver til frame pointer i næste activation record), R16-R23 til lokalevariable, og R24-R31 til indgaende parametre, herunder R30 som frame pointer.

(Disse regler hænger sammen med brugen af registre vinduer — se [Tan] eller [Sta] fordetaljer).

Endvidere indeholder RISC-processoren nogle flag, dvs. enkelte bit, som fortæller om re-sultatet af den sidste aritmetiske operation. Saledes kan vi se, om den sidste operation gavnul, eller et positivt tal, etc. Disse oplysninger bruges især ved betingede hop.

De mulige instruktioner i en RISC er af følgende former (hvor r1, r2, . . . angiver vilkarligeregistre og c en vilkarlig konstant):

1) Aritmetiske operationer af formen r1 := r2 op r3, hvor op er en operation som addi-tion, subtraktion, multiplikation, division, logisk and, logisk or, logisk xor eller logisknor.

2) Aritmetiske operationer af type r1 := r2 op c, hvor op er en operation som additioneller subtraktion. Det varierer lidt, om andre operationer tillades her.

128

Page 129: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

3) Load-operationer af formen M[r1+c] := r2, hvor vi pa adresse r1 + c skriver værdienaf r2. Disse operationer er fortrinlige til at skrive til lokale variable, der er spildt over ivores stack frame.

4) Store-operationer af formen r1 := M[r2+c], hvor vi aflæser værdien af hukommelses-celle r2 + c og placerer denne værdi i register r1. En god made at tilga variable i stackframe’n pa.

5) Ubetingede og betingede hop. Ved ubetingede hop angives adressen, hvortil vi hopper,ved betingede hop angives en kombination af flag, som, hvis alle disse flag er sande, faros til at hoppe til den efterfølgende adresse.

6) Call- og return-instruktioner, som bruges ved procedure-kald.

Med disse operationer er det en smal sag at oversætte fra 3AC til RISC-kode:

Vi antager i første omgang, at alle parametre, lokale variable og temporære variable kanplaceres i registre. Selve registerallokeringen finder sted i kapitel 14, men i en typisk RISC-processor vil de fleste variable sagtens kunne være i et register. Sa vi antager altsa, at debefinder sig der, uden at vi specificerer hvilket, og kun hvis en værdi spildes over i hukom-melsen, henter eller gemmer vi eksplicit denne værdi.

3AC-instruktioner af formen x := y op z oversættes direkte til den tilsvarende RISC-instruktion, mens unære operationer som fx. x := y kan oversættes ved at anvende det spe-cielle 0-registr R0 til x := R0 - y.

Kopi-instruktioner anvender ligeledes R0: x:=y oversættes til x := y + R0, og tildeling afkonstante værdier ligeledes: x := 3 oversættes til x:0 R0 + 3.

Adresserings- og pointer-instruktioner erstattes af en række aritmetiske operationer, opslagi symbol-tabellen og load- eller store-operationer.

Ubetingede hop oversættes uden videre, mens betingede hop oversættes til to instruktioner:I den første far vi sat flagene, og i den anden laver vi selve det betingede hop. Saledes bliverif a < b goto L oversat til de to instruktioner:

R0 := a - b

if negative jump to LInstruktioner relateret til procedurekald er lidt mere kildne: Idet vi jo har en RISC-

processor, placeres de første mange (dvs. i praksis alle) parametre i registre før procedu-rekaldet, hvorefter vi caller. Ved et return placeres en evt. retur-værdi i et register, hvoreftervi returner. Vi ma sa overlade det til registerallokatoren at finde ud af, om alle parametrefaktisk kan placeres i registre eller ej.

Endelig bør det her bemærkes, at ved mange RISC-processorer har vi de sakaldte register-vinduer, som gør procedure-kald meget lettere — men detaljerne afhænger helt af den konkreteprocessor.

12.2 CISC-processorer

Ved CISC-processorer afhænger detaljerne væsentligt mere af selve processorens opbygning oginstruktionssæt. Sa lad os kort skitsere situatione for en konkret processor, nemlig Pentium:

I Pentium-processoren er der 8 generelle registre, eax, ebx, ecx, edx, esi, edi, esp og ebpsamt flag som ved RISC-processoren.esp anvendes som stack pointer og ebp som frame pointer, og det bør man ikke ændre ved.

129

Page 130: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

esi og edi er gode som pointere ind i hukommelsen, især ved string-operationer, eax er densakaldte akkumulator og er god ved aritmetiske operationer (sammen med edx), osv.

Generelt kan man sige, at registrene i Pentium pa ingen made er ligestillede.

Instruktionerne i Pentium er mange, men de vigtigste og mest generelle er følgende (hvorr1, r2 etc som før er vilkarlige registre og c er en konstant):

1) Operationer af formen r1 := r2

2) Additioner etc. af formen r1 = r1 op r2. Operationen op er addition, subtraktion, and,or eller xor.

3) Logisk negering r1 := not r1. Incrementering af et register, inc r1. Dekrementeringaf et register, dec r1.

4) Multiplikation eller division, eax:edx := eax:edx op r1, hvor bade registrene eax ogedx involveres. Ved multiplikation placeres de laveste 32 bits af resultatet i eax og dehøjeste i edx. Ved division placeres kvotienten i eax og resten i edx.

5) Stack-operationer push r1 og pop r1, hvor stakken, r1 og stack pointeren esp modifi-ceres passende.

6) Tilgang til hukommelsen: M[r1 + c] := r2 og r2 := M[r1 + c].

7) De sædvanlige ubetingede og betingede hop.

8) De sædvanlige call og return-instruktioner.

Endvidere tillader Pentium mange andre mader, hvorpa hukommelsen kan tilgas, endovenstaende, men de kan i første omgang trygt ignoreres.

Kode-generering til en CISC-processor foregar næsten som til en RISC-processor: Hverenkelt 3AC-instruktion oversættes til en eller flere assembler-instruktioner. Dog er der følgendeforskelle:

1) De fa registre er intet problem, idet registerallokeringsalgoritmerne nok skal vide atoptimere situationen mest muligt.

2) 3AC-instruktioner af formen t1 := t2 op t3 kan ikke oversættes til en enkelt instruk-tion. I stedet oversætter vi til følgen af Pentium-instruktioner t1 := t2, t1 := t1 op

t3.

3) Multiplikationer og divisioner er lidt kildne, men heldigvis sjældne. Vi oversætter t1

:= t2 op t3 til følgen eax := t2, eax := eax * t3, t1 := eax og husker pa, atværdien af edx blev ødelagt ved multiplikationen — dette spiller en vis rolle ved regi-sterallokeringen og især under den sakaldte levetidsanalyse.

4) Komplekse instruktioner, andre mader at adressere hukommelsen pa etc. ignoreres. Vikan evt. under den sakaldte peephole optimering oversætte givne Pentium-instruktionertil disse, hvis ønskeligt.

12.3 Javas virtuelle maskine

Dette bliver kun en kort introduktion til JVM og JVM’s instruktionssæt. Mange flere detaljerkan findes i [Eng], som ogsa er den ultimative reference til Oolong, en ”assembler”til JVM.Specifikationen af JVM findes i [LY].

130

Page 131: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Javas virtuelle maskine er i sin opbygning radikalt anderledes fra konventionelle CPU’ere.Dels er JVM en virtuel maskine, hvilket betyder, at der i virkeligheden er tale om et program,der simulerer en processor, dels har JVM ingen registre (i hvert tilfælde ingen, som bruge-ren direkte kan tilga), men benytter en stak til sine beregninger, og endelig skjuler JVM’sinstruktionssæt mange detaljer omkring køretidsomgivelserne for os.

Disse forskelle beror i sidste ende pa designkriterierne for JVM: JVM’s primære formal ersikkerhed fremfor effektivitet.

Vi vil i det følgende introducere de forskellige faciliteter i JVM samtidigt med Oolong, somer en assembler, der genererer Java bytecode. Men som en smagsprøve kan vi starte med atbetragte følgende Java-program:

public class HelloWorld {

public static void main(String[] args) {

System.out.println("Hello, world!");

}

}

og den tilsvarende Oolong-kode:

.class public HelloWorld

.super java/lang/Object

.method public static main([Ljava/lang/String;)V

.limit stack 2

.limit locals 1

getstatic java/lang/System/out Ljava/io/PrintStream;

ldc "Hello, world!"

invokevirtual java/io/PrintStream/println (Ljava/lang/String;)V

return

.end method

.end class

Lad os gennemga denne kode linie for linie:

I den første linie, .class public HelloWorld, har vi et sakaldt direktiv — en kommando,som fortæller Oolong-assembleren et eller andet, der ikke som sadan genererer kode. Alledirektiver starter med et punktum. Dette direktiv fortæller samænd blot Oolong, hvad navnetpa class-filen er, og denne linie svarer nøje til Java-koden public class HelloWorld.

I den næste linie fortæller vi explicit Oolong, hvilken superklasse HelloWorld skal have— i Java antages det implicit, at hvis en klasses superklasse ikke angives, sa er superklassenjava.lang.Object , men i Oolong skal vi angive en superklasse. Af uransagelige grunde ernotationen java.lang.Object fra Java erstattet med java/lang/Object.

I linien med .method-direktivet deklarerer vi en metode. Metoden hedder main og er badepublic og static. Metodens returtype er void, hvilket kan ses af det sidste V i linien, ogmetoden modtager som parameter et array ([) af objekter (L) af typen java.lang.String .

I almindelighed forkortes Java’s typer i Oolong med et enkelt bogstav — se nedenstaendebogstav — arrays angives ved et foranstillet [, og (referencer til) objekter ved et L efterfulgtat klassens navn og et semikolon.

131

Page 132: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

type bogstav type bogstavbyte B int Iboolean Z long Jchar C reference Ldouble D short Sfloat F void V

Direktivet .limit stack 2 fortæller, at vi i metoden main kun far brug for to pladser pametodens activation record til temporære variable, mens .limit locals 1 angiver, vi kunhar en lokal variabel.

Disse direktiver bør angives, idet Oolong selv gætter, hvis de ikke er der. Og de skalangives korrekt — kommer vi f.eks. til at skrive .limit stack 3 i en metode, hvor stakkenfar størrelsen 4 eller mere, sa risikerer vi, at JVM nægter at udføre metoden.

Linien getstatic java/lang/System/out Ljava/io/PrintStream; henter den statiskeattribut out i klassen java.lang.System —dette er i øvrigt en reference til en java.io.PrintStreamog placerer den øverst pa stakken.

Linien ldc "Hello, World!" genererer en reference til strengen ”Hello, World!”og placererdenne reference øverst pa stakken.

Linien invokevirtual java/io/PrintStream/println (Ljava/lang/String;)V kaldermetoden println pa out-objektet og med parametren øverst pa stakken, nemlig referencen til”Hello, World!”. Vi er nødt til at angive, hvillken signatur den kaldte metode har — det kunnejo være, at der var flere overridede metoder med navnet println i java.io.PrintStream .

Ved selve metodekaldet poppes parametren og out-references af stakken, og da metoden ervoid, placeres der ingen retur-information pa stakken.

De sidste to linier er direktiver, der afslutter henholdsvis metoden main og klassen Hel-loWorld .

Nar vi skal generere kode til JVM, sa viser det sig, at 3AC ikke er rigtigt velegnet sommellemrepræsentation. Problemet ligger i, at 3AC ikke skelner mellem temporære variable,som kun bruges et enkelt sted i programmet, og almindelige variable. I JVM ville vi placererigtige variable i en af maskinens lokale variable, mens temporære variable ville blive placeretpa stakken som mellemresultater.

Vil man alligevel oversætte fra 3AC til Java Bytecode (eller Oolong’s assemblerkode), sakan man enten vælge at placere alle variable, temporære savel som rigtige, i lokale variable,men dette giver en meget ineffektiv kode. Alternativt kan man scanne 3AC-koden igennemfor at se, hvor en variabel faktisk anvendes: Anvendes den kun et sted, sa er det en temporærvariabel, og den kan placeres pa stakken, men anvendes den flere steder, sa skal den placeresi en lokal variabel.

En anden, og bedre mulighed er at oversætte direkte fra AST’et til Oolong assembler-kodeved en traversering af AST’et. Dette foregar nogenlunde pa samme made som oversættelsenfra AST’et til 3AC i forrige kapitel, dog skal udtryk behandles lidt anderledes. Ulempen veddenne fremgangsmade er, at vi ikke længere kan foretage mange af de optimeringer, som erudviklet og beskrivet i litteraruren, og som passer specielt godt pa 3AC.

132

Page 133: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

13 Registerallokering

En af de sidste faser i compileringen er registerallokeringen: Hvordan far vi placeret de forskel-lige variable og temporære variable i registre — helst saledes, at registrene udnyttes optimalt?

Dette problem er løseligt, men løsningen er en lille smule kompliceret: Først skal vi analysereprogrammet for at se, hvornar de forskellige variable er i live, dernæst opbygges den sakaldteinterferens-graf, og ud fra denne kan vi allokere registrene ved en graf-farvnings-algoritme.

Vi vil i eksemplerne ikke anvende den konkrete maskinkode, men derimod notationen framellemrepræsentationen — som vi sa i sidste kapitel, er der ikke den store forskel mellemden enkelte 3AC-instruktion og den enkelte konkrete maskinkodenotation, og for at undga atfordybe os i detaljer i CPU’ens arkitektur og opbygning vælger vi altsa 3AC. Men læserenbedes være opmærksom pa, at det altsa er pa maskinkoden, at vi registerallokerer.

Registerallokering er i virkeligheden en optimering, og mange af teknikkerne og algorit-merne, som udvikles i dette kapitel, kan ogsa anvendes i andre optimeringssammenhænge —se næste kapitel.

13.1 Ideen bag registerallokering

Lad os illustrere registerallokeringsprocessen med et simpelt eksempel.Betragt koden for en simpel procedure:

a := 0

L : b := a + 1

c := c + b

a := b * 2

if a < N goto L:

return c

I denne procedure har vi variablerne a, b og c (og en konstant N). Pa nuværende tidspunktved vi ikke, om disse tre variable skal placeres i activation record’en for proceduren eller iregistre — placerer vi en variabel i hukommelsen, siger vi, at variablen spildes. (Udtrykketstammer fra det engelske a spilled variable — som nok bedst oversættes til ”en variabel, derflyder/løber over”, men ”spildt variabel”virker mere mundret.)

Det første skridt i vores analyse er at lave en control-flow graf. Dette er en orienteret graf,hvori hver enkelt instruktion svarer til en knude, og der er en kant fra knude i til knude j,hvis computeren efter at have udført instruktion i kan udføre instruktion j som den næsteinstruktion.

Control-flow grafen for vores procedure bliver som pa figur 44.Næste skridt er at undersøge, hvornar de tre variable a, b, og c er i live — en variabel er i

live pa et sted i programmet, hvis den her har en værdi, der skal bruges senere.F.eks. er variablen a i live fra instruktion 1 til instruktion 2, idet vi i instruktion 2 faktisk

bruger værdien af a. Men a dør i instruktion 2 — værdien benyttes ikke i instruktion 3 eller4, og i instruktion 4 overskrives den gamle værdi i a. Variablen a er igen i live fra instruktion4, gennem instruktion 5 og tilbage til instruktion 2, hvor den igen dør. Levetiden for a er altsa1 → 2, 4 → 5 → 2.

Tilsvarende ser vi, at levetiden for b er fra instruktion 2 → 3 → 4, og for c fra 1 → 2 →3 → 4 → 5 → 2 (eller 5 → 6 — husk, at c anvendes i den sidste return-instruktion).

133

Page 134: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

0�a

1�� ab

bcc ��

2*ba �

Na �

return c

1

2

3

4

5

6

Figur 44

Vi kan illustrere disse levetider grafisk: Pa figur 45 er levetiden for de enkelte variableoptrukket med kraftige pile.

I almindelighed kan vi foretage denne sakaldte levetidsanalyse algoritmisk ved at løsesakaldte dataflow equations.

Næste skridt er at konstruere interferensgrafen. I denne graf er knuderne variable, og toknuder er forbundne med en kant, hvis der findes en instruktion, hvor de to variable er i livesamtidigt.

Ideen med denne graf er følgende: Hvis to variable er i live samtidigt, sa kan de ikke eksisterei det samme register, og de tilsvarende knuder er derfor forbundne.

Interferensgrafen farves nu. Farvning af en graf er et klassisk, velkendt problem, som garud pa at tildele hver enkelt knude i grafen en farve, saledes at to naboknuder ikke far sammefarve.

Vi er naturligvis ligeglade med, om variablen a bliver rød eller bla, men pointen er, atvi kan oversætte farver til registre — alle variable med samme farve kan placeres i sammeregister.

Interferensgrafen for vores eksempel bliver ganske simpel — se figur 46, og det er en smalsag at farve denne graf — c far en farve, mens a og b far den samme, anden farve.

Dette betyder, at a og b kan deles om det samme register, f.eks. R1, mens c skal placeres iet andet register, f.eks. R2. Vi kan derfor modificere vores kode til nedenstaende:

R1 := 0

L : R1 := R1 + 1

R2 := R2 + R1

R1 := R1 * 2

if R1 < N goto L:

return R2

134

Page 135: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

0�a

1�� ab

bcc ��

2*ba �

Na �

return c

1

2

3

4

5

6

0�a

1�� ab

bcc ��

2*ba �

Na �

return c

1

2

3

4

5

6

0�a

1�� ab

bcc ��

2*ba �

Na �

return c

1

2

3

4

5

6

levetid for blevetid for a levetid for c

Figur 45

a

b

c

a

b

c

Figur 46

13.2 Levetidsanalyse

I levetidsanalyse undersøger vi, hvilke variable der er i live hvor. Ideen er, at hver gang vianvender en variabel a, sa er den i live fra den knude, hvori a anvendes, og tilbage i flow-controlgrafen, indtil vi nar til en definition af a.

For at kunne formalisere dette ræsonnement, ma vi indføre noget terminologi:

Definition 70Lad n være en knude i flow-control grafen. Mængden af n’s umiddelbare efterfølgere iflow-control grafen betegnes succ(n). Mængden af n’s umiddelbare forgængere i flow-control grafen betegnes pred(n).

Eksempel 71For flow-control grafen pa figur 44 har vi, at succ(5) = {1, 6}, mens pred(5) = {4}.Endvidere har vi, at succ(2) = {3}, mens pred(2) = {1, 5}.

Definition 72

135

Page 136: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

For en knude n i control-flow grafen med den tilhørende instruktion s definerer vifølgende mængder af variable: kill(s) = mængden af de variable, hvis værdi ændresi s, og gen(s) = mængden af de variable, hvis værdi anvendes i s

Man skal tænke sig, at mængden af variable, der er i live pa et sted i flow-control grafen,glider baglæns i grafen. Hver gang vi kommer til en instruktion, hvor en variabels værdiændres, slas denne variabel ihjel, og vi er helt sikre pa, at den værdi af variable, vi havdebrug for længere nede ad strømmen, ikke findes før vores kill-instruktion.

Omvendt kan statements generere nye variable, der kan flyde med baglæns i grafen. Dettesker, hver gang en variabel anvendes.

Vi vil som regel forveksle en knude med den tilsvarende instruktion.Lad os opskrive en tabel over typiske instruktioner og deres gen- og kill-mængder:

Instruktion s gen(s) kill(s)t := a op b {a, b} {t}t := M[a] {b} {t}M[a] := b {b} {}if (a>b) goto L {a, b} {}goto L {} {}f(a,b,c) {a, b, c} {}t := f(a,b,c) {a, b, c} {t}

M[a] betyder, at vi tilgar hukommelsen pa den adresse, som star i variabel a, og f er enprocedure.

Definition 73For en knude n i flow-control grafen defineres out(n) som de variable, som er i live langsen kant, som forlader n, mens in(n) er de variable, som er i live langs en kant, som garind i n.

Vi kan nu opskrive dataflow ligningerne for levetidsanalyse:

Sætning 74Lad n være en knude i control-flow grafen, Sa gælder:

in(n) = gen(n) ∪ (out(n)− kill(n)) og out(n) =⋃

s∈succ(n)

in(s)

Hvorfor nu det? Jo, den første ligning siger, at de variable, der er i live lige efter n, mavære de variable, der er i live lige før n panær hvad n slar ihjel, sammen med de variable, somblev skabt i n. Den anden ligning siger, at alle de variable, der er i live inden en umiddelbarefterfølger til n nødvendigvis ogsa ma være i live lige efter n — dataflowet kan ikke ændrespa kanterne, kun i knuderne.

I praksis anvendes disse dataflow-ligninger saledes:

1) Lav en tabel over alle knuder i control-flow grafen

2) Beregn succ og pred for alle knuder i grafen

136

Page 137: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

3) Beregn gen og kill for alle knuder i grafen

4) Sæt in(n) og out(n) lig med {} for alle knuder

5) Anvend dataflow-ligningerne og modificer in- og out-mængderne

6) Gentag skridt 5), indtil in- og out-mængderne stabiliseres

Eksempel 75Lad os gennemføre levetidsanalyse pa control-flow grafen fra sektion 14.1. Til at startemed har vi tabellen:

n pred(n) succ(n) gen(n) kill(n) in(n) out(n)1 - 2 a2 1,5 3 a b3 2 4 b, c c4 3 5 b a5 4 2,6 a6 5 - c

Vi anvender nu in-ligningen fra sætning 74:

n pred(n) succ(n) gen(n) kill(n) in(n) out(n)1 - 2 a2 1,5 3 a b a3 2 4 b, c c b, c4 3 5 b a b5 4 2,6 a a6 5 - c c

og dernæst out-ligningen:

n pred(n) succ(n) gen(n) kill(n) in(n) out(n)1 - 2 a a2 1,5 3 a b a b, c3 2 4 b, c c b, c b4 3 5 b a b a5 4 2,6 a a a, c6 5 - c c

Efter næste anvendelse af in- og dernæst out-ligningen fas:

n pred(n) succ(n) gen(n) kill(n) in(n) out(n)1 - 2 a c a2 1,5 3 a b a, c b, c3 2 4 b, c c b, c b4 3 5 b a b a5 4 2,6 a a, c a, c6 5 - c c

Endnu en iteration:

137

Page 138: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

n pred(n) succ(n) gen(n) kill(n) in(n) out(n)1 - 2 a c a, c2 1,5 3 a b a, c b, c3 2 4 b, c c b, c b4 3 5 b a b, c a, c5 4 2,6 a a, c a, c6 5 - c c

og endnu en:

n pred(n) succ(n) gen(n) kill(n) in(n) out(n)1 - 2 a c a, c2 1,5 3 a b a, c b, c3 2 4 b, c c b, c b, c4 3 5 b a b, c a, c5 4 2,6 a a, c a, c6 5 - c c

og i den næste iteration ser vi, at situationen er stabiliseret.

Naturligvis er det dumt at betragte knuderne i samme rækkefølge som i control-flow grafen,for dataflowet gar jo modsat kanterne i grafen. Det viser sig, at laver man den sammen analyse,men tager knuder i modsat rækkefølge, sa opnar vi den stabile situation efter meget færreiterationer. Endvidere er det en god ide at først beregne out-mængden for en knude, ogdernæst in-mængden for den samme knude.

Eksempel 76Lad os lave levetidsanalyse som før, men med knuderne i omvendt rækkefølge:

n pred(n) succ(n) gen(n) kill(n) in(n) out(n)6 5 - c c5 4 2.6 a a4 3 5 b a b3 2 4 b, c c b, c2 1,5 3 a b a1 - 2 a

Første iteration:

n pred(n) succ(n) gen(n) kill(n) in(n) out(n)6 5 - c c5 4 2.6 a a, c a, c4 3 5 b a b, c a, c3 2 4 b, c c b, c b, c2 1,5 3 a b a, c b, c1 - 2 a c a, c

og det viser sig, at nu har situationen stabiliseret sig.

138

Page 139: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

I praksis er det ikke helt indlysende, hvilken rækkefølge, der er den bedste, men en godtommelfingerregel er at lave en dybde-først traversering af grafen, og anvende den omvendterækkefølge i sin levetidsanalyse.

Lad os tage et mere kompliceret eksempel:

Eksempel 77Betragt nedenstaende kode, der skal opfattes som en del af et større program:

live-in: j og k

1: g := M[j + 12]

2: h := k - 1

3: f := g * h

4: e := M[j + 8]

5: m := M[j + 16]

6: b := M[f]

7: c := e + 8

8: d := c

9: k := m + 4

10: j := b

liveout: d, j og k

Eftersom der ingen forgreninger er i koden, udelades pred og succ af nedenstaendeskema.

Det viser sig, at eftersom der ingen forgreninger er, konvergerer in- og out-mængderneefter kun to iterationer. I nedenstaende tabel har vi in- og out-mængderne efter 1.iteration i 4. og 5. kolonne, og efter 2. iteration i 6. og 7. kolonne.

n gen(n) kill(n) in(n) out(n) in(n) out(n)10 b j k, d, b k, j, d k, d, b k, j, d9 m k m m, d, b k, d, b8 c d c c,m, b m, d, b7 e c e e,m, b c,m, b6 f b f f, e,m e,m, b5 j m j j, e, f e,m, f4 j e j j, f j, e, f3 g, h f g, h g, h, j j, f2 k h k g, k, j h, g, j1 m g k, j k, j k, g, j

13.3 Opbygning af interferens-grafen

Vi skal nu se pa, hvorledes interferens-grafen opbygges. Princippet er klart nok: To variableforbindes med en kant, hvis de er i live samtidigt.

Uheldigvis giver denne regel anledning til alt for mange sammenligninger mellem alle muligepar af variable, og en nemmere regel er:

139

Page 140: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Hver gang vi har en instruktion n med, sa skal vi tilføje knuder fra a til alle variable xsom ligger i out(n). Pa den made behøver vi kun at se pa hver variabel en gang i variablenslevetid nemlig lige før den dør.

En anden komplikation er kopi-instruktioner af typen j := b, som der optræder bl.a. ieksempel 77. Sadanne instruktioner optræder faktisk hyppigt — enten stammer de tilbage frakodegenereringen, eller ogsa kan de opsta under nogle af de optimeringer, som vi skal se pa inæste kapitel.

Disse kopi-instruktioner er som regel overflødige, og det ville være lækkert, hvis vi kunnekomme af med dem. Faktisk ville det allerbedste være, hvis vores grafallokeringsalgoritmekunne komme til at placere j og b i samme register, for i sa fald kan vi uden videre slettekopi-instruktionen.

For at tage højde for dette indfører vi en ny type kanter i vores graf, nemlig kopi-kanter(som vi ofte vil tegne som stiplede linier). Hver gang vi har en kopi-instruktion af typen a :=

b, tegner vi en kopi-kant mellem a og b og haber pa, at vi senere kan sammensmelte de tovariable a og b.

Bemærk, at der intet er til hinder for, at der kan være bade en almindelig interferens-kantog en kopi-kant mellem to variable. Betragt f.eks. situationen nedenfor:

a := b

a := a + 1

x := b * 2

y := a - 18

Den første instruktion skaber kopi-kanten mellem a og b, men allerede i næste linie far aog b forskellig værdi, og da begge variable bruges senere, er de i live efter anden instruktion,og vi far derfor en interferens-kant mellem de to. I denne situation kan vi naturligvis ikkesammensmelte a og b.

Generelt dannes interferens-grafen ud fra følgende:

Definition 78Interferens-grafens knuder bestar af samtlige variable i koden. For hver enkelt instruk-tion n tilføjer vi kanter efter følgende regler:

1: Hvis n ikke er en kopi-instruktion, tilføjer vi kanter mellem a og x for alle a ∈kill(n) og x ∈ out(n)

2: Hvis n er en kopi-instruktion af formen a := b, sa tilføjer vi kanter mellem a ogx for alle x ∈ out(n) panær b, og en stiplet kopi-kant mellem a og b.

3: Er der bade en almindelig kant og en kopi-kant mellem to variable, sa slet kopi-kanten — de to variable kan alligevel aldrig sammensmeltes

Eksempel 79Interferens-grafen for koden i eksempel 77 bliver nedenstaende som pa figur 47.

140

Page 141: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

f

e

j k b m

d

ch g

Figur 47

13.4 Farvning af interferens-grafen

Ideen med at farve en graf stammer tilbage fra et klassisk matematisk problem, fire-farveproblemet :

I 1852 observerede den engelske kartograf Francis Guthrie, at man tilsyneladende altidkunne nøjes med fire farver, nar man skulle farve landene i et kort, saledes at to lande, derstødte op til hinanden, ikke matte fa samme farve. (Prøv selv efter!) Guthrie’s bror Frederick,der var matematiker, forsøgte at bevise dette, men forgæves.

Gennem arene lykkes det at bevise, at 6 og senere 5 farver var nok, men først i 1976 bevisteK. Appel og W. Haken, at det var nok med fire farver. Beviset var stærkt kontroversielt: Appelog Haken reducerede samtlige landkort til 1476 situationer. Hver af disse situationer blev saindtastet i en computer, der sa leverede en fire-farvning.

Det kontroversielle la i, at det var første gang, en computer direkte blev anvendt i etmatematisk bevis. Et af kritikpunkterne gik ud pa, at det ikke var sikkert, at programmet,som blev brugt i beviset, faktisk var korrekt, og rigtigt nok kom Appels og Hakens bevis iflere udgaver, hvor der hver gang blev rettet smafejl i deres anvendte algoritme.

Men der er i dag absolut ingen tvivl om, at Appels og Hakens bevis holder.

Et af de vigtigste (og letteste) skridt er at oversætte kort-problemet til et graf-teoretiskproblem: Hver enkelt land pa landkortet svarer til en knude, og to knuder forbindes, hvis deto tilsvarende lande har en fælles grænse.

Fire-farve-problemet bliver sa: Kan man tildele hver knude i grafen en farve, saledes at tonaboknuder ikke far samme farve?

I almindelighed kan man ikke farve en graf med 4 farver — de grafer, som genereres ud fralandkort er sakaldte planare grafer, som er grafer, der kan tegnes pa et stykke papir, saledesat ingen kanter skærer hinanden. Grafen i sektion 14.1 er planar, mens grafen fra eksempel77 absolut ikke er planar, idet kanterne j-d og k-g og kanterne b-c og d-m skærer hinanden.

Indenfor registerallokering er den typiske situation følgende: Vi har en CPU med k til-gængelige registre. Kan vi k-farve vores interferensgraf, saledes at alle variable kan placeres

141

Page 142: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

i de k registre? Og kan vi ikke det, og bliver nødt til at spilde en eller flere variable over ihukommelsen, hvorledes kan vi da farve de resterende variable pa den mest optimale made?

Der findes algoritmer, som løser ovenstaende problem eksakt, men uheldigvis har vi herfat pa et sakaldt NP-fuldstænsigt problem, hvilket i praksis betyder, at de bedste algoritmerkører i eksponentiel tid i antallet af knuder i grafen. Ved compileringen af ganske almindeligeprogrammer far vi udførselstider, der varer fra uger til ar, og dette er ikke acceptabelt.

Man vælger derfor at anvende en approximativ algoritme — den leverer normalt ikke detbedste resultat, men resultatet er som regel godt nok.

Algoritmen i sin simpleste form, hvor vi ignorerer move-instruktioner og sammensmeltnin-ger af knuder, gar ud pa følgende:

Sætning 80For at k-farve en interferens-graf gør vi følgende:

1) Fjern rekursivt knuder, der har mindre end k kanter. Bliv ved med dette, salængeder findes knuder med mindre end k kanter.

2) Vi har nu en graf, hvori alle knuderne har mindst k kanter. Fjern nu den knude,der er mest generende — den tilsvarende variabel regner vi nu med skal spildesover i hukommelsen, men det er ikke sikkert.

3) Fortsæt med skridt 1 og 2, indtil der ikke er flere knuder.

4) Vi kan nu føje knuderne tilbage i grafen i modsat rækkefølge af den, hvori knuderneblev fjernet, og farve dem samtidigt med. Det er ikke noget problem at farveknuderne, der blev fjernet i skridt 1 — da knuden har under k naboer kan vi altidfinde en farve, som ikke er blandt naboerne, da vi jo har flere farver end naboer. Vikan maske endog farve knuder, der blev fjerne i skridt 2, og i sa fald er vi lykkelige,men ofte vil vi indse, at vi ikke kan tildele disse knuder en farve, og variablernema spildes over i hukommelsen.

Har vi faktisk nogle variable, som spildes i skridt 4), sa er vi nødt til at modificere voreskode, saledes at der explicit er instruktioner, som henter og gemmer vores spildte variabel,hver gang den skal bruges, og starte forfra med levetidsanalyse og graf-farvning for det nyeprogram. I praksis viser det sig, at det nye programs interferensgraf kan farves uden besvær,sa vi slipper for yderligere iterationer.

I øvrigt kalder vi en knude, der har mindre end k naboer for insignifikant.

Eksempel 81Lad os illustrere algoritmen pa grafen fra eksempel 77 — hvor vi indtil videre ignorererkopi-kanterne. Vi vil 4-farve grafen. Algoritmen foregar i skridt, og grafens udseende kanefter hvert skridt ses pa figur 48 nedenfor:

1) Den oprindelige graf

2) Knuderne h, g, f og c er umiddelbart insignifikante, og kan fjernes

3) I den nye graf er e, m, j og k insignifikante og kan fjernes

4) De to sidste knuder er insignifikante og kan fjernes. Vi kan nu begynde at tilføjeknuder igen, og vi kan umiddelbart bringe b og d tilbage og farve dem

5) Vi kan nu bringe e, m, j, k tilbage og farve disse

142

Page 143: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

f

e

j k b m

dch g

e

j k b m

d

b

d

b

d

1

2e

j k b m

d

1

2

334

f

e

j k b m

dch g

2

1

2

334

1 2

3 4

5 6

1

412

2

Figur 48

143

Page 144: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

6) De sidste knuder bringes tilbage og farves

Uheldigvis tager algoritmen i sætning 80 ikke hensyn til, at knuder eventuelt kan sammen-smeltes, hvilket ellers er en ønskelig ting.

Et problem er dog, at vi ikke bare kan sammensmelte knuder, uden maske at komme til atødelægge vores muligheder for at farve grafen, og det er da bedre at undga at spilde variablei hukommelsen og sa maske bruge et register mere end nødvendigt end det omvendte!

En situation, hvor en sammensmeltning kan ødelægge farvingen er f.eks., nar vi sammen-smelter to knuder, der hver især har k − 1 forskellige naboer. Hver for sig er de to knuderinsignifikante og kan uden problemer farves, men tilsammen har den sammensmeltede knudemere end k naboer og er et potentielt problem.

Det er svært at sige, hvornar præcist vi kan sammensmelte to knuder uden at ødelæggevores farvningsmuligheder, men følgende regler gar vi ikke galt led i byen:

Sætning 82Vi kan sikkert sammensmelte to knuder, som er forbundet med en kopi-kant, i følgendesituationer:

Briggs kriterium: Knuderne a og b kan sammensmeltes, hvis den kombinerede knudehar mindre end k signifikante naboer.

Georges kriterium: Knuderne a og b kan sammensmeltes, hvis enhver nabo til a entenogsa allerede er en nabo til b, eller ogsa er insignifikant.

Vores algoritme bliver nu:

Sætning 83For at k-farve en interferens-graf med kopi-kanter gør vi følgende:

1) Fjern rekursivt knuder, der har mindre end k kanter, og som ikke har kopi-kanter.Bliv ved med dette, sa længe der findes knuder med mindre end k kanter.

2) Sammensmelt alle par af knuder, der har en kopi-kant mellem sig, og som opfylderenten Briggs’ eller Georges kriterium. Vend tilbage til skridt 1.

3) Vi har nu ikke længere insignifikante knuder uden kopi-kanter. Vælg derfor enknude med kopi-kanter, og frys denne knude, dvs. slet kopi-kanterne fra denneknude. Ga tilbage til skridt 1.

4) Vi har nu en graf, hvori alle knuderne er signifikante og der er ingen kopi-kantertilbage. Fjern nu den knude, der er mest generende — den tilsvarende variabelregner vi nu med skal spildes over i hukommelsen, men det er ikke sikkert. Ga tilskridt 1.

5) Der er nu ikke flere knuder. Tilføj nu knuderne til grafen i modsat rækkefølge afden, hvori de blev fjernet, og farv knuderne undervejs.

Eksempel 84Anvender vi algoritmen i sætning 83 pa grafen fra eksempel 77, fas resultatet pa figur49.

144

Page 145: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

f

e

j k b m

dch g

j b

dc

jb

cd

1

2

f

e

k m

h g

jb

cd

1

2

3

4

3

3

2

12

3 4 2

Figur 49

1) Den oprindelige graf. Vi fjerner de insignifikante knuder f , e, m, h, g og k

2) j og g, og c og d kan sammensmeltes uden videre

3) Den nye graf farves nemt

4) Vi tilføjer og farver de gamle knuder

Den nye kode bliver:

1) r2 := M[r1 + 12]

2) r3 := r3 - 1

3) r4 := r2 * r3

4) r2 := M[r1 + 8]

5) r3 := M[r1 + 16]

6) r1 := M[r4]

7) r2 := r2 + 8

8) r2 := r2

9) r3 := r3 + 4

10) r1 := r1

og vi kan nu nemt fjerne de overflødige instruktioner 8 og 10.

145

Page 146: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

I praksis optræder der endnu en komplikation: Vi har maske under kodegenereringen mattetbruge konkrete registre i vores foreløbige maskinkode. Knuderne svarende til disse registrekalder vi præ-farvede knuder — da der jo er tale om konkrete, fysiske registre, har de joallerede en ”registerfarve”tilknyttet sig.

Heldigvis virker algoritmen i sætning 83 uden videre ogsa med præ-farvede knuder — viskal dog huske pa, at præ-farvede knuder ikke ma fjernes fra grafen i skridt 1, og de ma ikkesammensmeltes med andre præ-farvede knuder i skridt 3.

Et sidste eksempel — denne gang med præ-farvede knuder:

Eksempel 85Betragt C-koden:

int f(int a, int b) {

int d = 0;

int e = a;

do {

d = d + b;

e = e - 1;

} while (e > 0);

return d;

}

og den tilsvarende 3AC-kode:

enter: c := r3

c := r1

b := r2

d := 0

e := a

loop: d := d + b

e := e - 1

if (e>0) goto loop

r1 := d

r3 := c

return (r1 og r2 live-out)

Som det ses er vi i en CPU med kun tre registre, r1, r2 og r3. Ved procedurens kaldindeholder r1 og r2 de to parametre, mens r3 indeholder anden information, som skalvære i r3 efter procedurens afslutning. r1 skal ved afslutningen indeholde retur-værdien.

Det ses, at interferens-grafen bliver som pa figur 50.I denne graf er skridt 1, 2 og 3 i algoritmen i sætning 83 ganske ineffektive, sa vi ma

spilde en af variablerne a, b, c, d eller e. Vi beregner en størrelse, spild-prioriteten sp,som følger: sp er et mal for, hvor mange gange knuden anvendes udenfor løkken og in-denfor løkken (vi antager helt vilkarligt, at løkken gennemløbes 10 gange) divideret medknudens grad. Ideen er, at en knude, der bruges meget, helst ikke skal spildes, samtidigtmed, at en knude med mange naboer er mest generende i algoritmen. Beregningen ernaturligvis en tommelfingerregel, og man kan sikkert finde pa bedre kriterier:

146

Page 147: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

r1

r2

r3

a

b

c

d

e

Figur 50

knude anv. udenf. løkke anv. i løkke grad spa 2 0 4 0.50b 1 1 4 2.75c 2 0 6 0.33d 2 2 4 5.50e 1 3 3 10.33

c far den laveste sp, og vi spilder derfor c. Efter at have fjernet c bliver den nye grafsom pa figur 51.

r1

r2

r3

a

b

d

e

Figur 51

Der er ingen insignifikante knuder, der umiddlebart kan fjernes, men vi kan sammen-smelte a og e efter George’s kriterium — se figur 52.

r1

r2

r3

ae

b

d

Figur 52

Vi kan nu sammensmelte r2 og b eller r1 og d. Lad os sammensmelte r2 og b for ikkeat komme i karambolage med kopi-kanten mellem r1 og ae — figur 53.

147

Page 148: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

r1

r2br3

ae

d

Figur 53

r2br3

r1ae

d

Figur 54

Lad os sammensmelte r1 og ae — figur 54.Uheldigvis kan vi ikke sammensmelte r1ae og d, idet der er en interferens-kant mellem

ed to knuder, men vi kan fjerne d uden videre.Den resulterende graf er faktisk allerede farvet, og nar vi tilføjer d, kan vi farve d

samme farve som r3.Uheldigvis — nar vi tilføjer c, sa kan c ikke farves. Vi ma derfor spilde c.Pa’en igen: Den nye kode bliver:

enter: c1 := r3

M[cloc] := c1

a := r1

b := r2

d := 0

e := a

loop: d := d + b

e := e - 1

if e > 0 goto loop

r1 := d

c2 := M[cloc]

r3 := c2

return

hvor vi gemmer c i hukommelsen pa position cloc. Hver gang vi henter eller gemmerc, sker det via en ny temporær variabel, c1 eller c2.

Det viser sig, at denne nye kode giver en interferensgraf, der sagtens kan farves: a oge farves r1, b farves r2, og c1, c2 og d farves r3. Disse registerallokeringer giver koden:

enter: r3 := r3

M[cloc] := r3

148

Page 149: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

r2 := r2

r3 := 0

r1 := r1

loop: r3 := r3 + r2

r1 := r1 - 1

if r1 > 0 goto loop

r1 := r3

r3 := M[cloc]

r3 := r3

return

og fjerner vi alle overflødige instruktioner, fas

enter: M[cloc] := r3

r3 := 0

loop: r3 := r3 + r2

r1 := r1 - 1

if r1 > 0 goto loop

r1 := r3

r3 := M[cloc]

return

Opgaver

Opgave 13.1 Betragt følgende program:

1: m := 0

2: v := 0

3: if v >= n goto 15

4: r := v

5: s := 0

6: if r < n goto 9

7: v := v + 1

8: goto 3

9: x := M[r]

10: s := s + x

11: if s <= m goto 13

12: m := s

13: r := r + 1

14: goto 6

15: return m

a) Tegn flow-control grafen

b) Lav levetidsanalyse pa programmet, og tegn interferensgrafen

c) Programmet skal køre pa en processor med 4 registre. Lav en registerallokering

149

Page 150: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Opgave 13.2 Nedenstaende program er compileret til en maskine med tre registre r1, r2 ogr3. Selve programmet er en procedure f , som anvender registret r1, overskriver r1 og r2, mensom selv sørger for at gemme og reetablere værdien af r3.

f: c := r3 t := r1p := r1 u := s + tif p = 0 goto L1 goto L2r1 := M[p] L1: u := 1call f L2: r1 := us := r1 r3 := cr1 := M[p + 4] returncall f

Lav levetidsanalyse pa dette program, konstruer interferens grafen, og alloker registre.

Opgave 13.3 Nedenstaende tabel repræsenterer en interferens-graf pa tabelform. Knuderne1–6 er præ-farvede registre, mens A–H er almindelige variable. Alle præ-farvede knuder inter-fererer, og de almindelige knuder interferer med de knuder, hvor der er ’x’ i tabellen.

A B C D E F G H

1 x x x x x x2 x3 x x x x x x4 x x x x x x5 x x x x x x6 x x x x x xA

B

C x x x x xD x x x x xE x x x x xF x x x x xG x x x x xH x x x x x

Følgende knudepar er forbundne med kopi-kanter: (A, 3), (H, 3), (G, 3), (B, 2), (C, 1), (D, 6),(E, 4) og (F, 5). CPU’en indeholder i alt 8 registre.

a) Farv denne graf vha. algoritmen i sætning 80.b) Farv denne graf vha. algoritmen i sætning 83

150

Page 151: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

14 Optimering af kode

Et skridt, som i teorien kan undgas, men som i praksis er uundværligt — især for professionellecompilere — er kodeoptimeringen.

Malet er naturligvis at skrive sa hurtig og sa pladsbesparende kode som muligt. Det visersig at være nemmere at lade alle de andre faser i compileringen ignorere disse krav, og opereremed ret ineffektiv kode, og sa optimere denne kode i separate optimeringsfaser.

Kodeoptimeringen kan forega forskellige steder i compileringen — i forbindelse med AST’et,i forbindelse med mellemrepræsentationen og i forbindelse med den endelige malkode.

Kodeoptimering pa niveau af AST og mellemrepræsentation er temmeligt maskinuafhængigog foregar som regel pa et højt abstraktionsniveau, og hertil findes der en række generelle algo-ritmer, mens kodeoptimering pa malkoden er pa et meget lavt niveau og kræver et indgaendekendskab til mal-CPU’en.

Vi vil kun kort illustrere forskellige optimeringsteknikker og henvise læseren til f.eks. [Muc]eller [ASU] for yderligere detaljer indenfor dette meget store omrade.

14.1 Overblik

Lad os kort betragte nogle af de forskellige optimeringsformer:

14.1.1 Foldning af konstanter og konstant-propagation

Betragt 3AC-koden:

T1 := 3

T2 := 4

T3 := T1 * T2

Her kunne vi erstatte det hele med

T1 := 3

T2 := 4

T3 := 12

og spare os for en multiplikation. Maske er vi endda sa heldige, at T1 og T2 ikke brugesmere i den efterfølgende kode, vi kan sa skære disse to temporære variable helt væk!

Konstant-propagation er noget af samme skuffe. Hvis vi ved, at en variabel pa et sted iprogrammet tildeles en værdi, og variablen lidt senere helt sikkert har samme værdi, sa kanvi erstatte forekomsten af variablen med den tilsvarende værdi.

Dette giver maske i sig selv ingen optimering, men maske kan vi senere folde nogle kon-stanter, eller maske bliver variablens levetid mindre, sa der bliver mulighed for at opna enbedre registerallokering.

151

Page 152: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

14.1.2 Kopi-propagation

Antag, at vi har en kopi-instruktion af typen a := b. Vi kan da erstatte alle forekomster af amed b (eller omvendt), sa længe vi er sikre pa, at a eller b ikke har skiftet værdi. (Dette kanundersøges ved dataflow analyse).

I sig selv optimererer dette maske ikke, men giver maske mulighed for at senere at kunnefjerne a. Samtidigt bliver det muligvis lettere at lave en bedre registerallokering.

14.1.3 Elimination af død kode

Hvis vi kan konstatere, at noget kode aldrig kan udføres, eller vi har nogle overflødige variable,sa væk med det.

Det er sjældent, at der optræder død kode i kildekoden — de fleste programmører skriverkun det allermest nødvendige, men død kode kan optræde efter at vi har gennemført nogle afde andre optimeringer, f.eks. kopi-propagation eller konstant-propagation.

14.1.4 Fælles deludtryk

Betragt kildekoden: temp = a[i]; a[i] = a[j]; a[j] = temp; , som bytter om pa to ele-menter i et array. Oversættes dette til 3AC, far vi noget i stil med:

T1 := i

T2 := T1 * elemSize(a)

T3 := &a + T2

temp := *T3

T4 := j

T5 := T4 * elemSize(a);

T6 := &a + T5

T7 := i

T8 := T7 * elemSize(a)

T9 := &a + T8

*T9 := *T6

T10 := temp

T11 := j

T12 := T11 * elemSize(a)

T13 := &a + T12

*T13 := T10

Bemærk, at udtrykkene T1 og T7, T2 og T8, T3 og T9, T4 og T11, T5 og T12, og T6 ogT13 beregner nøjagtigt de samme størrelser. Det ville være bedre, hvis vi f.eks. kun beregnedeT3/T9 en gang, og huskede pa resultatet (især hvis ovenstaende kode star inde i en løkke, savi spilder en multiplikation og addition mange gange).

Ved en algoritme, som vi skal illustrere senere, kan vi faktisk huske pa, at T3/T9 alleredeer beregnet, sa vi kan spare udregningen af T9.

Det er især ved array-manipulationer, at vi kan eliminere allerede beregnede udtryk, medteknikken kan faktisk anvendes med fordelmange steder.

Med alle besparelser far vi det ca. halvt sa store stykke kode:

152

Page 153: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

T1 := i

T2 := T1 * elemSize(a)

T3 := &a + T2

temp := *T3

T4 := j

T5 := T4 * elemSize(a)

T6 := &a + T5

*T3 := *T6

T10 := temp

*T6 := T10

14.1.5 Løkke-invarianter

Ofte er vi i den situation, at en størrelse beregnes igen og igen inde i en løkke. Det kunnef.eks. være adresse-beregningerne til et array som i eksemplet ovenfor.

Optimeringen gar ud pa at flytte denne beregning uden for løkken, sa den kun beregnes engang.

14.1.6 Reduktion i styrke

Ofte kan man erstatte en række instruktioner med en anden, der gør det samme, men mereeffektivt.

Saledes er det dyrt at multiplicere, og multiplikation kan ofte erstattes med addition: T2:= 2 * T1 erstattes med T2 := T1 + T1.

Hvis man multiplicerer med en to-potens, hvad der er meget almindeligt i array-adresse-beregninger, da elemSize af et array ofte er 2, 4 eller 8, sa kan multiplikationen erstattes afendnu mere effektive bit-shift-operationer.

Et andet eksempel er potensopløftning, der ofte kan erstattes af multiplikation.Som regel foretages reduktion i styrke direkte pa den endelige maskinkode og udnytter

særegenheder ved mal-CPU’ens instruktionssæt.

Det generelle mønster er, at en enkelt af ovenstaende optimeringer som regel give anled-ning til, at vi kan gennemføre optimeringer af andre typer bagefter. Derfor vælger man i enoptimerende compiler ofte af gennemføre forskellige former for optimeringer efter hinanden,og ofte gentages en type optimering op til flere gange.

153

Page 154: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Løsninger til udvalgte opgaver

Eventuelle fejl er helt pa læserens eget ansvar...Opgave 2.1

a) a(a|b)∗b b) (a|b)∗b(a b)∗b(a|b)∗b(a|b)∗c) (a|b)∗abab(a|b)∗ d) a((a|b)(a|b))∗e) a((a|b)(a|b))∗|b(a|b)((a|b)(a|b))∗ f) (ε|a|b)(ε|a|b)(ε|a|b)(ε|a|b)g) ε|a|b|ab|bb|ba|(a|b)(a|b)(a|b)(ab)∗ h) ε|a|b|ab|ba|bb|aab|(a|b)b(a|b)|(a|b)4(a|b)∗i) a(a|b|ε)|(a(a|b))∗|(a(a|b))∗a j) aaa∗|baaa∗|a∗abaa∗|a∗aab

Opgave 2.2

a) alle strenge starter og slutter med a b) alle mulige strengec) alle strenge med længde 3 d) strenge af formen ambn med m,n ≥ 0e) alle mulige strenge f) alle strene bestaende af kun a eller kun bg) strenge af formen a3m med m ≥ 0 h) strenge, som ender pa aaai) alle mulige strenge j) ikke-tomme strenge, som ikke starter med ba

Opgave 2.3 ε, ab og a2b2 accepteres.

Opgave 2.4 (a(ab)∗b)∗

Opgave 2.5 a(ba)∗

Opgave 2.6 a((ab)∗(ba)∗)∗

Opgave 2.7

Opgave 2.8 abba, bababa ,b4ab4. Det regulære udtryk er (a|b)∗b(a|ε)b(a b)∗.Opgave 2.9

a bA = {1} A BB = {1, 2, 3} C DC = {1, 3} A DD = {1, 2, 3, 4} C D

A er starttilstanden, og D er den eneste sluttilstand.

Opgave 3.1

a) D ⇒ DS ⇒ 7S ⇒ 7DS ⇒ 78S ⇒ 78DS ⇒ 780S ⇒ 780D ⇒ 7801

b) D ⇒ DS ⇒ DDS ⇒ DDDS ⇒ DDDD ⇒ DDD1 ⇒ DD01 ⇒ D801 ⇒ 7801

c) Alle positive heltal samt 0.

Opgave 3.2 {a2n | n ≥ 0}Opgave 3.3 {a} ∪ {abnc | n ≥ 1}Opgave 3.4

S → ε | bbS

154

Page 155: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

a

b

b

a

b

a

b b b

a

a

a

a

b a,b

a

a

b b a

a,ba

b

a,b

a,b

a

b

a

b b

a,b a,ba,b

a b

c

d e

f g

Figur 55

Opgave 3.5

S → a | bsOpgave 3.6

S → ε | abSOpgave 3.7

b) S ⇒ S(S)S ⇒∗ ()S ⇒ ()S(S)S ⇒∗ ()()

S ⇒ S(S)S ⇒∗ S() ⇒ S(S)S() ⇒∗ ()()

c) S → (S)S | ε

Opgave 4.1

155

Page 156: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

stak input output$S (x+ x) ∗ x$ S → AS ′

$S ′A (x+ x) ∗ x$ A→ BA′

$S ′A′B (x+ x) ∗ x$ B → (S)$S ′A′)S( (x+ x) ∗ x$$S ′A′)S x+ x) ∗ x$ S → AS ′

$S ′A′)S ′A x+ x) ∗ x$ A→ BA′

$S ′A′)S ′A′B x+ x) ∗ x$ B → x$S ′A′)S ′A′x x+ x) ∗ x$$S ′A′)S ′A′ +x) ∗ x$ A′ → ε$S ′A′)S ′ +x) ∗ x$ S ′ → +AS ′

$S ′A′)S ′A+ +x) ∗ x$$S ′A′)S ′A x) ∗ x$ A→ BA′

$S ′A′)S ′A′B x) ∗ x$ B → x$S ′A′)S ′A′x x) ∗ x$$S ′A′)S ′A′ ) ∗ x$ A′ → ε$S ′A′)S ′ ) ∗ x$ S ′ → ε$S ′A′) ) ∗ x$$S ′A′ ∗x$ A′ → ∗BA′

$S ′A′B∗ ∗x$$S ′A′B x$ B → x$S ′A′x x$$S ′A′ $ A′ → ε$S ′ $ S ′ → ε$ $$S x+ $ S → AS ′

$S ′A x+ $ A→ BA′

$S ′A′B x+ $ B → x$S ′A′x x+ $$S ′A′ +$ A′ → ε$S ′ +$ S ′ → +AS ′

$S ′A′+ +$$S ′A′ $ error$S ∗+ x$ error

Opgave 4.2Uden venstre-faktorisering:

nullable first follow a b $S nej a $ S → aA og S → aBA nej a $ A→ aB nej b $ B → b

Med venstre-faktorisering

156

Page 157: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

nullable first follow a bS nej a $ S → aS ′

S ′ nej a, b $ S ′ → a S ′ → BA nej a $ A→ aB nej b $ B → b

Opgave 4.3a

nullable first follow a b c $S ja a, c b, $ S → Asb S → C S → C S → CA nej a a, b, c A→ aC ja c b, $ C → ε C → cC C → ε

Opgave 4.3b

nullable first follow a b $S nej a, c b$ S → aSb S → ε S → ε

Opgave 4.3c

nullable first follow a b c $S nej a, c b, $ S → aSB S → CB nej b b, $ B → bC nej c b, $ C → c

Opgave 4.4

a) S → a | bA A→ a | bab) S → aS | bc) S → aAc | bAc A→ aAc | bAc | ε

Opgave 4.5Den nye grammatik bliver

lexp → atom|listatom → number | identifier

list → ( ls )

ls → lexp l’

l’ → lexp l’ | ε

med

157

Page 158: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

nullable first followlexp nej number identifier ( $ number identifier (atom nej number identifier $ number identifier (list nej ( $ number identifier (ls nej number identifier ( )l’ ja number identifier ( )

og med parsetabellen:

( ) number identifier $lexp lexp → list lexp → atom lexp → atomatom atom → number atom → identifierlist list → (ls)ls ls → lexp l’ ls → lexp l’ ls → lexp l’l’ l’ → lexp l’ l’ → ε l’ → lexp l’ l’ → lexp l’

Opgave 4.6Den nye grammatik bliver

lexp → atom|listatom → number | identifier

list → ( ls )

ls → lexp , l’

l’ → , l’ | ε

med

nullable first followlexp nej number identifier ( $ ,atom nej number identifier $ ,list nej ( $ ,ls nej number identifier ( )l’ ja , )

og med parsetabellen:

( ) number identifier , $lexp lexp → list lexp → atom lexp → atomatom atom → number atom → identifierlist list → (ls)ls ls → lexp , l’ ls → lexp , l’ ls → lexp , l’l’ l’ → ε l’ → lexp , l’

Opgave 4.7Den nye grammatik bliver

158

Page 159: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

decl → type vl

type → int | float

vl → identifier , vl’

vl’ → , vl | ε

med

nullable first followdecl nej int float $type nej int float identifiervl nej identifier $vl’ ja , $

og med parsetabellen:

int float , identifier $decl decl → type vl decl → type vltype type → int type → floatvl vl → identifier , vl’vl’ vl’ → , vl vl’ → , vl | ε

Opgave 4.8Den nye grammatik bliver

statement → assignment | other call’

assignment → identifier = exp

call’ → ε | (exp-list )

Opgave 5.1DFA’en pa tabelform er:

a b c d S A BS ′ → ·S$ S → ·aA S → ·bB 0 2 7 1S ′ → ·$ 1S → a · A A→ ·cA A→ ·d 2 5 4 3S → aA· 3A→ d· 4A→ c · A A→ ·cA A→ ·d 5 5 4 6A→ cA· 6S → b ·B B → ·cB B → ·d 7 9 10 8S → bB· 8B → d· 9B → c ·B B → ·cB B → ·d 10 10 9 11B → cB· 11

159

Page 160: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Parsetabellen bliver

a b c d $ S A B0 s2 s7 g11 r0 r0 r0 r0 r02 s5 s4 g33 r1 r1 r1 r1 r14 r4 r4 r4 r4 r45 s5 s4 g66 r3 r3 r3 r3 r37 s9 s10 g88 r2 r2 r2 r2 r29 r6 r6 r6 r6 r610 s10 s9 g1111 r5 r5 r5 r5 r5

Parsingen af strengene accd og bcd bliver:

stak input handling stak input handling$0 accd$ shift 2 $0 bcd$ shift 7$0a2 ccd$ shift 5 $0b7 cd$ shift 10$0a2c5 cd$ shift 5 $0b7c10 d$ shift 9$0a2c5c5 d$ shift 4 $0b7c10d9 $ reduce 6$0a2c5c5d4 $ reduce 4 $0b7c10B11 $ reduce 5$0a2c5c5A6 $ reduce 3 $0b7B8 $ reduce 2$0a2c5A6 $ reduce 3 $0S1 $ accept$0a2A3 $ reduce 1$0S1 $ accept

Opgave 5.2DFA’en pa tabelform er:

a b c S AS ′ → ·S$ S → ·aAc S → ·b 0 3 2 1 1S ′ → ·$ 1S → ·b 2S → a · Ac A→ ·aSc A→ ·b 3 5 6 4A→ aA · c 4 8A→ a · Sc S → ·aAc S → ·b 5 3 2 7A→ b· 6A→ aS · c 7 9S → aAc· 8A→ aSc· 9

Parsetabellen bliver

160

Page 161: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

a b c $ S A0 s3 s2 g11 r0 r0 r0 r02 r2 r2 r2 r23 s5 s6 g44 s85 s3 s2 g76 r4 r4 r4 r47 s98 r1 r1 r1 r19 r3 r3 r3 r3 r3

Opgave 5.3Automatens tilstande er

nummer poster0 S ′ → ·S$ S → ·aAd S → ·bBd S → ·bAe S → ·aBe1 S ′ → S · $2 S → a · Ad S → a ·Be A→ ·c A→ ·c3 S → b ·Bd S → b · Ae A→ ·c A→ ·c4 A→ ·c B → ·c5 S → aA · d6 S → aAd·7 S → aB · e8 S → aBe·9 S → bA · e10 S → bAe·11 S → bB · d12 S → bBd·

og med overgangene

a b c d e S A B0 2 3 112 4 5 73 4 9 114 6567 889 101011 1212

161

Page 162: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

LR(0)-parsetabellen:

a b c d e $ S A B0 s2 s3 g11 r0 r0 r0 r0 r0 r02 s4 g5 g73 s4 g9 g114 r5/r6 r5/r6 r5/r6 r5/r6 r5/r6 r5/r65 s66 r1 r1 r1 r1 r1 r17 s88 r3 r3 r3 r3 r3 r39 s1010 r4 r4 r4 r4 r4 r411 s1212 r2 r2 r2 r2 r2 r2

mens SLR(0)-parsetabellen bliver:

a b c d e $ S A B0 s2 s3 g11 r02 s4 g5 g73 s4 g9 g114 r5/r6 r5/r65 s66 r17 s88 r39 s101011 s1212 r2

Opgave 5.4LR(0)-DFA’en bliver pa tabelform:

poster a + SS ′ → ·S$ S → ·S + a S → ·a 0 4 2 1S ′ → S · $ S → S ·+a 1 2S → S + ·a 2 3S → S + a· 3S → a· 4

LR(0)-parsetabellen er:

162

Page 163: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

a + $ S0 s4 s2 g11 r0 s2/r0 r02 s33 r1 r1 r14 r2 r2 r2

SLR(0)-parsetabellen er:

a + $ S0 s4 s2 g11 s2 r02 s33 r1 r14 r2 r2

Opgave 5.5LR(0)-DFA’ens tilstande er

poster0 S ′ → ·S$ S → ·V = E S → ·E E → ·V V → ·x V → · ∗ E1 S ′ → S · $2 S → V · = E E → V ·3 S → E·4 V → x·5 V → ∗ · E E → ·V V → · V → · ∗ E6 S → V = ·E E → ·V V → ·x V → · ∗ E7 S → V = E·8 E → V ·9 V → ∗E·

med overgangene

= x ∗ S E V0 4 5 1 3 212 3345 4 5 9 86 4 5 7 8789

LR(0)-parsetabellen:

163

Page 164: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

= x ∗ $ S E V0 s4 s5 g1 g3 g21 r0 r0 r0 r02 r3/s6 r3 r3 r33 r2 r2 r2 r24 r4 r4 r4 r45 s4 s5 g9 g86 s4 s5 g7 g87 r1 r1 r1 r18 r3 r3 r3 r39 r5 r5 r5 r5

SLR(0)-parsetabellen:

= x ∗ $ S E V0 s4 s5 g1 g3 g21 r02 r3/s6 r33 r2 r24 r45 s4 s5 g9 g86 s4 s5 g7 g87 r1 r18 r3 r39 r5

LR(1)-DFA’en far følgende tilstand:

poster0 [S ′ → ·S$, ?] [S → ·V = E, $] [S → ·E, $] [E → ·V, $] [V → ·x,= $] [V → · ∗ E,= $]1 [S ′ → S · $, ?]2 [S → V · = E, $] [E → V ·, $]3 [S → E·, $]4 [V → x·, $ =]5 [V → ∗ · E,= $] [E → ·V,= $] [V → ·,= $] [V → · ∗ E,= $]6 [S → V = ·E, $] [E → ·V, $] [V → ·x, $] [V → · ∗ E, $]7 [S → V = E·,$]8 [E → V ·, $]9 [V → ·x, $]10 [V → ∗ · E, $] [E → ·V, $] [V → x, $] [V → ·E, $]11 [V → ∗e·,= $]12 [E → V ·,= $]13 [V → ∗e·, $]

og med overgangene

164

Page 165: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

= x ∗ S E V0 4 5 1 3 212 6345 4 5 11 126 9 10 7 878910 9 10 13 8111213

LR(1)-parsetabellen er:

= x ∗ $ S E V0 s4 s5 g1 g3 g21 r12 s6 r33 r24 r4 r45 s4 s5 g11 g126 s9 s10 g7 g87 r18 r39 r410 s9 s10 g13 g811 r5 r512 r3 r313 r5

LALR(1)-DFA’ens tilstande bliver:

poster0 [S ′ → ·S$, ?] [S → ·V = E, $] [S → ·E, $] [E → ·V, $] [V → ·x, $] [V → · ∗ E, $]1 [S ′ → S · $, ?]2 [S → V · = E, $] [E → V ·, $]3 [S → E·, $]4 [V → x·, $ =]5 [V → ∗ · E, $ =] [E → ·V, $ =] [V → ·, $ =] [V → · ∗ E, $ =]6 [S → V = ·E, $] [E → ·V, $] [V → ·x, $] [V → · ∗ E, $]7 [S → V = E·, $]8 [E → V ·, $ =]9 [V → ∗E·, $ =]

165

Page 166: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

med overgangene

= x ∗ S E V0 4 5 1 3 212 3345 4 5 9 86 4 5 7 8789

og med LALR(1)-parsetabellen:

= x ∗ $ S E V0 s4 s5 g1 g3 g21 r02 s6 r33 r24 r4 r45 s4 s5 g9 g86 s4 s5 g7 g87 r18 r3 r39 r5 r5

Opgave 5.6Ingen detaljer, men i LR(1)-DFA’en er der to tilstande {[A→ c·, d], [B → c·, e]} og {[A→

c·, e], [B → c·, d]}. Slas disse sammen, fas en reduce/reduce-konflikt.

Opgave 5.11Ingen detaljer, men problemet med grammatikken er, at vi ikke ved, hvarnar vi skal stoppe

med at shifte a’ere pa stakken og begynde at reduce i stedet.

Opgave 7.1

number → digit number.power = 0number → digit number number1.power = number2.power + 1

number1.val = 10number1.power ∗ digit.val + number2.valdigit→ 7 digit.val = 7

Opgave 7.2

dnum→ num.num dnum.value = num1.value+ num2.value · 10num2.count

num→ num digit num1.val = num2.val · 10 + digit.valnum1.count = num2.count+ 1

num→ digit num.val = digit.valnum.count = 1

digit→ 7 digit.val = 7

166

Page 167: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Opgave 7.5Orden: S.u,B.u,B.v, C.v, A.u,A.v, S.vS.u = 3 B.u = 3 B.v = 3 C.v = 1 A.u = 4 A.v = 8 S.v = 8

Opgave 7.6Det gar galt, a vi far en cykel i afhængighedsgrafen: A.u→ A.v → C.u→ C.v → A.u

Opgave 8.2Statisk scope: ”i = 11, j = 3, k = 4,m = 5”.Dynamisk scope: ”i = 11, j = 9, k = 4,m = 11”.

167

Page 168: Indhold - kennethhansen.net€™xzxzxzxzxzxz’. Sprogkandefineresp˚aadskilligem˚ader—entendirekte,vha.brugafmængdebyggernota-tionen,rekursivtvha.grammatikker,ellervha.regulæreudtryk

Litteratur

App: Andrew W. Appel: Modern Compiler Implementation in Java. Cambridge UniversityPress, 1998

ASU: Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullman: Compilers: Principles, Techniques andTools. Addison-Wesley, 1986

Eng: Joshua Engel: Programming for the Java Virtual Machine. Addison-Wesley, 1999

GBJL: Dick Grune, Henri E. Bal, J.H. Jacobs & Koen G. Langendoen: Modern Compiler De-sign. Wiley, 2000

Lar: Craig Larman, Rhett Guthrie: Java 2 Performance and Idiom Guide. PrenticeHall, 2000

LY: Tim Lindholm, Frank Yellin: The Java Virtual Machine Specification. Addison-Wesley,1997

Lou: Kenneth C. Louden: Compiler Construction: Principles and Practice. PWS, 1997

Muc: Steven S. Muchnick: Advanced Compiler Design & Implementation. Morgan Kauffman,1997

Par: Thomas W: Parsons: Introduction to Compiler Construction. W.H. Freeman, 1992

PZ: Terrence W. Pratt, Marvin V. Zelkowitz: Programming Languages. PrenticeHall, 2001

Seb: Robert W. Sebestra: Concepts of Programming Languages. Addison-Wesley, 2002

Sip: Michael Sipser: Introduction to the Theory of Computation. PWS, 1997

Sta: William Stallings: Computer Organization and Architecture. PrenticeHall, 2000

Sud: Thomas A. Sudkamp: Languages and Machines. Addison-Wesley, 1997

Tan: Andrew S. Tanenbaum: Structured Computer Organization. PrenticeHall, 1999

Ven: Bill Venners: Inside the Java Virtual Machine. McGrawHill, 1999

WB: David A. Watt, Deryck F. Brown: Programming Language Processors in Java. Prenti-ceHall, 2000

168