Semantyka i Weryfikacja Programów - Laboratorium 3 Modelowanie układów mikroprocesorowych - część II Wykonywanie całego programu Cały program wykonywany jest przez funkcję intpprog. Jedynym argumentem jest tu lista instrukcji, zaś stos jest początkowo pusty (patrz funkcja pomocnicza ips). fun intpprog(is) = let fun ips([],x::xs) = x (* koniec listy instrukcji (obliczeń) zwracany wierzchołek stosu *) ips(i::is,xs) = ips(is, intinstr(i,xs))(* wykonanie pojedynczej instrukcji i *) in ips(is,[]) end; (* start programu, stos początkowo pusty*) Jak widać, po wykonaniu wszystkich instrukcji funkcja intpprog zwraca wartość znajdującą się na wierzchołku stosu (początku listy). Przykład Obliczyć wartość wyrażenia: 2.0 * (3.0 + 4.0); Rozwiązanie 2. 0 4. 0 3. 0 A D D 7. 0 M U L 1 4. 0 Drzewo obliczeń pokazano powyżej. Pierwszą instrukcją do wykonania jest ADD z argumentami 3.0 i 4.0. Należy załadować na stos oba argumenty (PUSH 3.0, PUSH 4.0), a dopiero potem wykonać dodawanie ADD. Wynik działania znajduje się na stosie. Do wykonania mnożenia potrzebna jest wartość 2.0 (PUSH 2.0). Ostatnią operacją jest mnożenie - MUL. Kompletna lista instrukcji wygląda następująco: val il1 = [PUSH 3.0, PUSH 4.0, ADD, PUSH 2.0, MUL]; Wykonanie programu jest następujące: intpprog(il1); (* zanotuj wynik *) Zadanie. Wykonaj dwie niepoprawne listy instrukcji: - 1 -
val il3 = [PUSH 3.0, PUSH 4.0, ADD, MUL]; Jakie w nich występują błędy? Co zwraca funkcja intpprog? Sprawdzenie poprawności listy instrukcji analiza składniowa Do elementarnej analizy poprawności programu wykorzystuje się analizator składniowy - parser. Bada on, czy instrukcje i argumenty są poprawnie umieszczone w programie, ale nie wykonuje żadnych obliczeń. Również dla programów jak wyżej można wykonać elementarne sprawdzenie. Poprawna składniowo lista instrukcji powinna mieć dwie elementarne cechy: 1) dla każdej wykonywanej instrukcji muszą znajdować się argumenty na stosie, 2) po wykonaniu całego programu na stosie powinien pozostać tylko jeden element. Rozwiązanie. Poszczególne instrukcje wymagają różnej liczby argumentów i niejednakowo modyfikują stos. Przedstawia to poniższa tabela. instrukcja argumenty pobierane ze stosu modyfikacja stosu PUSH x 0 +1 ^^ 1 0 ADD, MUL, SUB 2-1 Na tej podstawie zdefiniowano następującą funkcję sprawdzającą: fun poprawny ([],1) = true (* lista instrukcji pusta, na stosie zostaje dokladnie 1 wartosc *) poprawny ([], n) = false (* lista instrukcji pusta po wykonaniu programu, ale zbyt duza pozostalosc na stosie *) poprawny ((PUSH _)::xs, n) = n>=0 andalso poprawny (xs,n+1) (* stos po wykonaniu instrukcji zwiększa się o 1 el.*) poprawny (( ^^ )::xs, n) = n>=1 andalso poprawny (xs,n) (* wszystkie pozostale operatory dwuargumentowe *) poprawny ( _ ::xs, n) = n>=2 andalso poprawny (xs,n-1); Sprawdzenie Zanotuj otrzymane rezultaty dla trzech poniższych przypadków. Czy są one prawidłowe? val il1 = [PUSH 3.0, PUSH 4.0, ADD, PUSH 2.0, MUL]; poprawny (il1,0); poprawny (il2,0); val il3 = [PUSH 3.0, PUSH 4.0, ADD, MUL]; poprawny (il3,0); - 2 -
Wyszukiwanie błędów podczas wykonywania programu. W przypadku, gdy w czasie wykonywania programu wystąpi błąd (dzielenie przez zero, nieprawidłowy argument funkcji itp.) zgłaszany jest wyjątek, którego obsługa pozwala na wykrycie przyczyny. Pomocne jest wypisanie zawartości stosu, bieżącej instrukcji oraz listy instrukcji pozostałych do wykonania. Funkcja pomocnicza 1 wypisywanie zawartości stosu. fun wypisz_stos ([]:stack) = "nil"^" " wypisz_stos (x1::xs) = (Real.toString x1) ^ " " ^ wypisz_stos(xs); Zadanie. Sprawdź działanie powyższej funkcji na dwóch przykładach. Funkcja pomocnicza 2 wypisywanie bieżącej instrukcji. fun wypisz_instr ( PUSH x) = "PUSH "^(Real.toString x) wypisz_instr ( ^^ ) = " ^^ " wypisz_instr ( ADD ) = " ADD " wypisz_instr ( SUB ) = " SUB " wypisz_instr ( MUL ) = " MUL "; Zadanie. Sprawdź działanie powyższej funkcji na dwóch przykładach. Realizacja interpretera z obsługą (wypisywaniem) błędów exception RUN_TIME_ERROR; (* blad podczas wykonywania programu *) fun intpprog(is) = let fun ips([],x::xs) = x ips(i::is,xs) = ips(is, intinstr(i,xs) handle BLEDNY_PROGRAM (ins, stos) => ( print "Bledny program\n"; print ("instrukcja -> "^(wypisz_instr ins) ^ "\n"); print (" stos po wykonaniu ===>>> "^wypisz_stos(stos)); raise RUN_TIME_ERROR (* dalsze wykonywanie programu jest przerywane *) ) ) in ips(is,[]) end; Funkcja wypisuje informacje o błędzie i przerywa działanie programu. Można ją rozszerzyć wprowadzając dodatkowy licznik instrukcji (ułatwi wykrycie błędu). Sprawdzenie Zanotuj otrzymane wyniki. Czy znalezienie błędu w programie jest możliwe? Wskaż przyczynę błędu. val il1 = [PUSH 3.0, PUSH 4.0, ADD, PUSH 2.0, MUL ]; intpprog (il1); intpprog (il2); - 3 -
val il3 = [PUSH 3.0, PUSH 4.0, ADD, MUL]; intpprog (il3); Część 2 kompilator Problem. Do podanego powyżej modelu układu obliczeniowego należy dopisać kompilator tworzący listę instrukcji dla danego wyrażenia. Podobnie jak w językach programowania wysokiego poziomu podaje się wyrażenie, którego wartość należy obliczyć, zaś jego tłumaczenie (kompilację) wykonuje specjalizowany program kompilatora. Rozwiązanie. Zdefiniowane zostały operatory: infix 6 ++ --; infix 7 ** ; oraz składnia wyrażeń: datatype fexpr = C of real (* stala Const *) ++ of fexpr * fexpr ** of fexpr * fexpr -- of fexpr * fexpr Pot2 of fexpr ; Dla tak podanych wyrażeń funkcja trans (odpowiednik kompilatora) tłumacząca wyrażenia na listę instrukcji jest następująca: fun trans( C x) = [PUSH x] trans(fe1 ++ fe2) = trans(fe1) @ trans(fe2) @ [ADD] trans(fe1 -- fe2) = trans(fe1) @ trans(fe2) @ [SUB] trans(fe1 ** fe2) = trans(fe1) @ trans(fe2) @ [MUL] trans(pot2 (fe)) = trans(fe) @ [^^] ; Przykład. Obliczyć wartość wyrażenia 2.0 * (3.0 + 4.0); val wyraz = C 2.0 ** (C 3.0 ++ C 4.0); Zobacz, jak jest ono interpretowane przez ML (priorytet operatorów). Jest to właściwie program w języku wyższego rzędu, tłumaczony dalej na listę instrukcji. trans (C 2.0 ** ( C 3.0 ++ C 4.0) trans(c 2.0) @ trans (C 3.0 ++ C 4.0) @ [MUL] [PUSH 2.0]@ trans(c 3.0)@ trans(c 4.0)@ [ADD]@ [MUL] [PUSH 2.0]@ [PUSH 3.0]@ PUSH [4.0]@ [ADD]@ [MUL] Automatyczne tłumaczenie val il = trans (wyraz); Wykonanie skompilowanego programu. - 4 -
intpprog (il); Zadanie. Sprawdź, jak jest wykonywana translacja wyrażenia zawierającego nawiasy. Zadania do samodzielnego rozwiązania 1. Zmodyfikuj funkcję sprawdzającą poprawny tak, aby precyzyjnie informowała (przynajmniej dla jednego wariantu), która instrukcja jest błędna i z jakich powodów. Zastosuj funkcje pomocnicze: fun wypisz_stos ([]:stack) = "nil"^"\n" wypisz_stos (x1::xs) = (Real.toString x1)^" "^wypisz_stos(xs); fun wypisz_instr ( PUSH x) = "PUSH "^(Real.toString x) wypisz_instr ( ^^ ) = " ^^ " wypisz_instr ( ADD ) = " ADD " wypisz_instr ( SUB ) = " SUB " wypisz_instr ( MUL ) = " MUL "; 2. Zdefiniuj wielowariantową funkcję intinstr (drugim argumentem jest dowolna lista il) za pomocą konstrukcji case. Do wyodrębniania elementów listy użyj instrukcji postaci let val (x1::x2::xs) = il in...end; Uwzględnij generowanie wyjątków przy nieprawidłowych wywołaniach funkcji. 3. Zdefiniuj wielowariantową funkcję wypisz_instr za pomocą konstrukcji case. 4. Zmodyfikuj intpprog tak, aby zliczała wykonane instrukcje. Teraz funkcja powinna zwracać parę. - 5 -