1. Aho A.V., Ullman J.D. - The Theory of Parsing, Translation and Compiling.1972. 2. Foster J.M. - Automatyczna analiza składniowa. 1976 3. Gries D. - Konstrukcja translatorów dla maszyn cyfrowych, 1984 4. Hapgood F.R.A. - Metody kompilacji. 1982. 5. Waite W.M., Goos G. - Konstrukcja kompilatorów. 1989. 6. Aho A.V., Sethi R., Ullman J.D. - Compiler: Principles, Techniques and Tools. 1986. Wstęp: Translator - program, który umożliwia wykonanie programów napisanych w języku różnym od języka komputera. Rozróżnia się dwa rodzaje translatorów: a) kompilatory b) interpretery Ad. a). Kompilator - program, który tłumaczy program źródłowy na równoważny program wynikowy. Program źródłowy jest napisany w języku źródłowym, a program wynikowy należy do języka wynikowego. Wykonanie programu kompilatora następuje w czasie tłumaczenia. Cecha zasadnicza: program po przetłumaczeniu nie da się zmienić, jest statyczny. Kompilator jako dane wejściowe otrzymujemy "cały" program źródłowy i przekształca go na postać wynikowa. Program We Kompilator Wy Źródłowy Program wynikowy Ad. b). Interpreter można nazwać dynamicznym translatorem. Tłumaczy on na bieżąco i wykonuje program źródłowy. Działanie interpretera polega na wyodrębnieniu niewielkich jednostek programu źródłowego, tłumaczniu ich na pewna postać wynikowa i natychmiastowym ich wykonywaniu. Proces jest cykliczny. W czasie interpretacji przechowywany jest program źródłowy. Przykład: for i:=1 to 5 do s := s + i ; w przypadku intepretera ten fragment jest 5-krotnie tłumaczony Zalety: a)kompilatory -program wynikowy wykonuje się szybciej -do wykonania programu wynikowego nie jest potrzebny kompilator b)interpretery -łatwość zmian programu -mniejsza zajętość pamięci zewnętrznej (tylko tekst źródłowy) -możliwość pracy konwersacyjnej (zatrzymanie wykonania, zmiana wartości zmiennych, kontynuacja wykonania)
Struktura kompilatora. Kompilator Program źródłowy Analiza Leksykalna SKANER Analiza Analiza syntaktycz.. ( i semant.) PASER Kod pośredni Generacja kodu wynikow. GENERA- TOR Synteza Optymaliza cja kodu OPTYMA- LIZATOR Program wynikowy Tablice zmiennych, stałych, etykiet, itd... (A) Analiza leksykalna ciąg znaków ciąg tokenów SKANER Przykład: we : COST:= ( PRICE + TAX ) * 0.98 wy : id 1 := ( id 2 + id 3 ) * num 4 Tablica symboli. Adres Nazwa Charakter Dodatkowe informacje 1 2 3 4 COST PRICE TAX 0.98 Zmienna Zmienna Zmienna Stała Zmienna-pozycyjna Zmienna-pozycyjna Zmienna-pozycyjna Zmienna-pozycyjna Wyjście skanera : <= Wejście skanera: (id,1) COST (egu, ) := (left-par, ) ( (id,2) PRICE (plus, ) + (id,3) TAX (right-par, ) ) (mult, ) * (num,4) 0.98
Analizator leksykalny SKANER Program Źródłowy Analizator leksykalny SKANER token Daj następny token Analizator syntaktyczny PARSER postać pośrednia Tablica symboli a) Realizacja szeregowa Program źródłowy SKANER PARSER... program wynikowy -translator wieloprzebiegowy -translator nakładkowy -mniejsza zajętość PaO b)realizacja równoległa PARSER Program źródłowy SKANER Generator Optymal. Podprogram podprogram program wynikowy -translator jednoprzebiegowy -większa zajętość PaO -krótszy czas kompilacji
Zadania skanera: -wyodrębnianie tokenów -ignorowanie komentarzy -ignorowanie białych znaków (spacji, tabulacji, znaków nowej linii...) -korelowanie błędów zgłaszanych przez kompilator z numerami linii (skaner może liczyć \n) -tworzenie kopii zbioru wejściowego (źródłowego) łącznie z komunikatami o błędach -czasami realizacja funkcji preprocessingu, rozwijanie makrodefinicji Rozdzielenie etapu analizy na dwie odrębne funkcje: analizę leksykalna i analizę syntaktyczną sprawia, że jedna i druga mogą być wykonywane przy użyciu bardziej efektywnych algorytmów, gdyż algorytmy te istotnie się różnią, wykorzystując inne pryncypia formalne i realizacyjne. Podstawowe pojęcia: token, leksem, wzorzec (pattern) Przykład: const pi2 = 6.2832; token leksem wzorzec (pattern) const id num const pi2 = 6.2832 const - ( słowo kluczowe ) litera (litera cyfra )* < > <= >= = > <> cyfra + (.cyfra + )?(E(+ -)?cyfra + )? źródło : const pi2 = 6.2832 leksemy: const pi2 = 6.2832 tokeny : const id num Wzorce są opisami sposobu wyodrębniania tokenów. Wzorce pełnią role produkcji w gramatykach leksykalnych. Tokeny są symbolami nieterminalnymi w gramatykach leksykalnych. Tokeny są terminalami w gramatyce syntaktycznej. Podział gramatyki G opisującej dany język programowania mający na celu oddzielenie analizy leksykalnej od analizy syntaktycznej. Gramatyka G języka źródłowego (najczęściej bezkontekstowa) G = < N, T, P, Z > gdzie na ogól T = { A, B,..., Z, a, b,..., z, 0, 1,..., 9, +, -, *, /,, (, ),..} Przebudowujemy gramatykę G na rodzinę gramatyk:
G s = < N s, T s, P s, Z s > - gramatyka syntaktyczna T s = { Ts i Ts i jest tokenem } tokeny są terminalami w gramatyce syntaktycznej G i = < N i, T i, P i, Z i > - gramatyki leksykalne jedna dla każdego tokenu Z i =Ts i - i-ty token jest symbolem początkowym (nieterminalem) i-tej gramatyki leksykalnej Ts i N i, T i T N = N s! n T =! n i= 1 T i i= 1 N i Gramatyka języka źródłowego G jest więc efektem "podstawienia" gramatyk leksykalnych G i P = P s! n P i do gramatyki syntaktycznej G s i= 1 Z = Z s Przykład: Gramatyka języka analizowanego G: G = < N = { <program>, <nazwa>, <liczba>, <symbol>, <+>, <*>} T = { a, b, c, 0, 1, +, * }, P = { <program> ::= <nazwa> <liczba> <program><symbol><program> <nazwa> ::= a b c <nazwa> a <nazwa> b <nazwa> c <nazwa> 0 <nazwa> 1 <liczba> ::= 0 1 <liczba> 0 <liczba> 1 <symbol> ::= <+> <*> <+> ::= + <*> ::= * }, Z = <program> > tokeny: id num op_add op_mul Gramatyka syntaktyczna: <nazwa>, <liczba>, <+>, <*> G s = < N s = { <program>, <symbol> }, id num op_add op_mul T s = { }, <nazwa>, <liczba>, <+>, <*> P s = { <programy> ::= <nazwa> <liczba> <program><symbol><program> <symbol> ::= <+> <*> }, Z s = <program> >
Gramatyki leksykalne: 1) gramatyka nazw id G 1 = < N 1 = { <nazwa> }, T 1 = { a, b, c, 0, 1 }, P 1 = { <nazwa> ::= a b c <nazwa> a <nazwa> <nazwa> b <nazwa> c 0 <nazwa> 1 } Z 1 = <nazwa> > 2) gramatyka liczb num G 2 = < N 2 = { <liczba> }, T 2 = { 0, 1 }, P 2 = { <liczba> ::= 0 1 <liczba> 0 <liczba> 1 } Z 2 = < <liczba> > 3) gramatyka plusa op_add G 3 = < N 3 = { <+> }, T 3 = { + }, P 3 = { <+> : := + }, Z 3 = <+> > 4) gramatyka gwiazdki op_mult G 4 = < N 4 = { }, <*> T 4 = { * }, P 4 = { <*> ::= * }, Z 4 =< <*> >
Jeżeli do gramatyki G s "podstawimy gramatyki G 1, G 2, G 3 oraz G 4 - otrzymamy pełną gramatykę G. W gramatykach leksykalnych tokeny są traktowanie jako nieterminale. W gramatyce syntaktycznej tokeny pełnią role terminali. Gramatyki leksykalne G i należą na ogół do klasy gramatyk regularnych. Niekiedy jednak pojawiają się odstępstwa od regularności. Przykład: Uwzględnienie prawego kontekstu w FORTRANie Spacje (z wyjątkiem spacji wewnątrz łańcuchów) są zawsze ignorowane. Porównujemy: 1) DO 5 I = 1.25 równoznaczne DO5I = 1.25 (instrukcja podstawienia) 2) DO 5 I = 1,25 instrukcja pętli typu "for", odpowiednik: for i:=1 to 25 do begin...... etykieta... 5:end; 1) DO 5 I = 1.25 identyfikator operator podstawienia stała 2) stałe DO 5 I = 1, 25 przecinek słowo kluczowe etykieta operator podstawienia DO identyfikator zmiennej sterującej pętli DO. 5 I = 1 \n ' Po przeczytaniu znaków DO nie można dokonać uzgodnienia tokenu "Słowo kluczowe DO" dopóki nie zbada się prawego kontekstu i nie znajdzie się przecinka (wtedy rzeczywiście uzgadnia się "DO") lub kropki bądź znaku nowej linii (wtedy mamy instrukcje podstawienia).
Przykład: Słowa kluczowe nie są zastrzeżone (FORTRAN, PL/I). W języku PL/I poprawny jest zapis: IF THEN THEN THEN = ELSE ; ELSE ELSE = THEN; Jeśli więcej niż jeden wzorzec odpowiada danemu tokenowi, skaner musi zwrócić dodatkową informacje. Przykład: (FORTRAN) E.LE. M * C ** 2 ( E <= mc2 ) E <id, pointer do tablicy symboli >.LE. <oper_rel, <= > M <id, pointer do tablicy symboli > * <oper_mult, nic > C <id, pointer do tablicy symboli > ** <oper_exp, nic > 2 <num, pointer do tablicy lub wartość > Do opisu wzorców dla skanera stosujemy definicje regularne d 1 -> r 1 d 2 -> r 2... d n -> r n gdzie: d i - unikalna nazwa, r i - wyrażenie regularne nad symbolami alfabetu terminalnego T { d 1, d 2,..., d i-1 } Przykład: stale bez znaku w Pascalu digit --> 0 1 2 3 4 5 6 7 8 9 digit --> digit digit* {jest to zastąpienie klasycznego zapisu: digits --> digits digit digit } optional_fraction -->.digits ε optional_exponent --> E ( + - ε ) digits ε num --> digits optional_fraction optional_exponent Symbole metajęzyka : --> * ( ) Uproszczenia w notacji: w + - oznacza jedno lub więcej wystąpień wzorca w w + ww* w? - oznacza zero lub jedno wystąpienie wzorca w w? w ε
Przykład: stałe bez znaku w Pascalu digit --> 0 1 2 3 4 5 6 7 8 9 digits --> digit+ optional_fraction --> (.digits)? optional_exponent --> ( E (+ -)? digits)? num --> digits optional_fraction optional_exponent Symbole metajęzyka : --> + *? ( ) Jeżeli już wiemy, jak specyfikować tokeny, a właściwie jak tworzyć ich wzorce, to teraz musimy się nauczyć rozpoznawać tokeny na podstawie wzorców.
Rozpoznawanie tokenów. Przykładowa gramatyka syntaktyczna: stmt --> if expr then stmt if expr then stmt else stmt ε expr --> term term term term --> id num Definicje regularne: delim --> \t \n ws --> delim + letter --> a... z A... Z digit --> 0 1... 9 if --> if then --> then else --> else --> < <= <> = > >= id --> letter (letter digit) * num --> digit + (.digit + )?(E(+ -)?digit + )? Analizator leksykalny ma zwracać następujące informacje: wyr. regul. token atrybut tokenu ws if then else id num < <= <> = > >= - if then else id num - - - - wskaźnik do tablicy symboli wskaźnik do tablicy symboli LT LE NE EQ GT GE
Diagram rozpoznawania tokenu start < = 0 1 2 return(,le) > 3 return(,ne) = 5 other * unput() 4 return(,lt) return(,eq) > = return(,ge) 6 7 other * unput() return(,gt) 8 Rozpoznawanie identyfikatorów digit letter start 9 letter 10 other * unput() 11 return(gettoken(),install_id()) gettoken() - funkcja sprawdza, czy lexem jest słowem kluczowym. Jeśli tak, zwraca odpowiedni token skojarzony z tym słowem kluczowym, np. if, then czy else. Jeśli nie, funkcja zwraca id. install_id() - funkcja sprawdza, przeszukując odpowiednia tablice, czy lexem jest słowem kluczowym. Jeśli tak, zwraca 0. W przeciwnym przypadku bada, czy taki lexem już figuruje w tablicy symboli. Jeśli tak zwraca wskaźnik do niego. Jeśli nie, dokonuje instalacji nowego lexemu w tablicy symboli i zwraca odpowiedni wskaźnik. Nie zawsze stosuje się metodę umieszczania słów kluczowych w tablicy. Można budować dla nich odrębne wzorce i konstruować automaty rozpoznające. Wówczas jednak liczba stanów analizatora gwałtownie rośnie.
Rozpoznawanie tokenu num other * 20 digit + digit digit digit E digit other * 12 13 14 15 16 17 18 E - digit 19 other * 21 W stanach 19 20 21 { unput() return(num,install_num()) instal_num() - funkcja umieszcza leksem w tablicy i zwraca wskaźnik do niego Ignorowanie białych znaków delim delim other * 22 23 24 unput() żadna inna akcja analizatora nie jest podejmowana