Metody realizacji języków programowania/MRJP Wykład 5
Autor: Artur Zaroda (zaroda@mimuw.edu.pl)
Realizacja funkcji i procedur
Instrukcja wywołania funkcji (dla procedur obowiązuje ten sam mechanizm) nakazuje rozpoczęcie wykonania funkcji, a po jej zakończeniu powrót do instrukcji za wywołaniem. Historię obliczeń w programie można przedstawić w postaci tzw. drzewa aktywacji, którego węzły reprezentują wcielenia (wykonania) funkcji (korzeń to program główny) a węzeł F ma synów G1...Gn jeśli wcielenie funkcji F wywołało G1, później G2 itd. Podczas wykonania programu odwiedzamy węzły tego drzewa w porządku obejścia w głąb, od lewej do prawej. Na ścieżce od korzenia do aktualnego węzła są wcielenia funkcji, które w danej chwili wykonują się, na lewo już zakończone a na prawo te, które jeszcze się nie rozpoczęły. Jeśli istnieje ścieżka, na której występuje wiele wcieleń tej samej funkcji (nie koniecznie obok siebie), mówimy że funkcja ta jest rekurencyjna.
Przekazywanie sterowania
Są dwa sposoby realizacji przekazywania sterowania między wołającym a wołanym.
- podprogram otwarty (inline, macro)
- kod funkcji/procedury wołanej wstawiamy w miejscu wywołania
- podprogram zamknięty
- przekazanie sterowania następuje za pomocą instrukcji skoku ze śladem
Metoda z zastosowaniem podprogramu otwartego może dać kod wynikowy bardziej efektywny, gdyż eliminuje instrukcje skoku. W większości przypadków powoduje ona jednak wydłużenie kodu wynikowego, a poza tym jest mniej uniwersalna - nie można jej zastosować do funkcji/procedur rekurencyjnych.
W dalszej części wykładu będziemy się posługiwali wyłącznie podprogramami zamkniętymi, czyli wywołanie z użyciem skoku ze śladem.
Rekord aktywacji
Z każdym wcieleniem funkcji wiążemy pewne informacje. Obszar pamięci, w którym są zapisywane, nazywamy rekordem aktywacji (albo „ramką”). W językach, takich jak Pascal lub C, rekordów aktywacji dla wcieleń funkcji, które już się zakończyły, nie trzeba przechowywać – potrzebne są tylko te, które znajdują się na aktualnej ścieżce w drzewie aktywacji.
Gdyby nie było rekurencji, dla każdej funkcji w programie moglibyśmy z góry zarezerwować obszar pamięci na jeden rekord, gdyż wiemy, że tylko tyle będzie potrzebnych. W językach dopuszczających rekurencję miejsce na rekordy aktywacji trzeba przydzielać w chwili wywołania funkcji a zwalniać je, gdy funkcja się skończy. Rekordy aktywacji przechowujemy więc na stosie.
Zawartość rekordu aktywacji
Informacje pamiętane w rekordzie aktywacji zależą m. in. od języka. Mogą tam być:
- parametry
- zmienne lokalne zadeklarowane przez programistę i zmienne tymczasowe
- ślad powrotu – informacja, do której instrukcji przekazać sterowanie po zakończeniu funkcji
- kopia rejestrów (w zależności od kompilatora przechowujemy wszystkie, niektóre lub żaden)
- łącze dynamiczne (DL, ang. dynamic link) – wskaźnik na poprzedni rekord aktywacji. Ciąg rekordów połączonych wskaźnikami DL tworzy tzw. łańcuch dynamiczny, czyli aktualną zawartość stosu. Można uniknąć przechowywania DL w rekordzie, ale zwykle się tego nie robi.
- łącze statyczne (SL, ang. static link) (tylko, jeśli język ze strukturą blokową) – opis poniżej
- miejsce na wynik
Postać rekordu aktywacji, a więc kolejność jego pól, nie jest sztywno określona - projektuje ją autor implementacji języka.
Adresowanie rekordu aktywacji
Aby funkcja podczas działania miała dostęp do swojego rekordu aktywacji, jego adres jest zwykle przechowywany w wybranym rejestrze. Pola rekordu aktywacji są adresowane przez określenie ich przesunięcia względem miejsca wskazywanego przez ten rejestr. Dzięki temu każde wcielenie funkcji, niezależnie od miejsca w pamięci, w którym znajduje się rekord aktywacji, w ten sam sposób może korzystać z jego zawartości, a więc wszystkie wcielenia mają wspólny kod.
Adresem rekordu aktywacji, a więc wartością, względem której adresowane są pola, nie musi być adres jego początku. Jeśli uznamy, że tak będzie wygodniej, możemy przyjąć, że adresem rekordu aktywacji będzie adres jednego z pól w środku tego rekordu. Przesunięcia poszczególnych pól będą w takie sytuacji wartościami ze znakiem - dodatnimi i ujemnymi.
W przypadku niektórych języków adresowanie rekordu aktywacji względem jego środka może być najwygodniejszym rozwiązaniem. Tak jest między innymi w języku C, w którym funkcja może być wywoływana ze zmienną liczbą argumentów. Gdyby pola rekordu aktywacji były adresowane względem jego początku i tam właśnie znajdowały się parametry, przesunięcia pól rekordu aktywacji zależałyby od liczby parametrów, a więc nie byłby znane podczas kompilacji. Rozwiązaniem tego problemu jest adresowanie rekordu aktywacji względem miejsca pomiędzy parametrami a zmiennymi lokalnymi funkcji. Dodatkowo parametry w C wkłada się do rekordu aktywacji w kolejności od ostatniego do pierwszego, dzięki czemu przesunięcie miejsca, w którym znajduje się K-ty parametr, nie zależy od liczby parametrów, tylko od stałej K.
Ponieważ aktualny rekord aktywacji zawsze znajduje się na wierzchołku stosu rekordów aktywacji, możliwe jest też użycie do jego adresowania rejestru będącego wskaźnikiem stosu. Rozwiązanie to pozwala na zaoszczędzenie jednego rejestru, ale to jest niewygodne, gdyż wierzchołek stosu przesuwa się podczas obliczeń, powodując zmiany przesunięć pól rekordu aktywacji.
Protokół wywołania i powrotu z funkcji
Protokół wywołania i powrotu z funkcji opisuje czynności, które w związku z wywołaniem funkcji ma wykonać wołający (zarówno przed przekazaniem sterowania do wołanego, jak i po powrocie) oraz to, co wołany (czyli każda funkcja) ma robić na początku i na końcu. Podstawowym zadaniem jest zbudowanie rekordu aktywacji oraz usunięcie go. Niektóre czynności z tym związane musi wykonywać wołający (np. liczenie parametrów), inne wołany (np. zarezerwowanie miejsca na zmienne lokalne), a jeszcze inne może wykonać zarówno wołany jak i wołający.
Zaprojektujemy protokół wywołania i powrotu z funkcji na maszynie stosowej opisanej powyżej. Założymy następującą postać rekordu aktywacji:
miejsce na wynik parametry ślad DL zmienne
Dla uproszczenia zakładamy, że miejsce na wynik znajduje się zarówno w rekordach aktywacji funkcji, jak i procedur. Modyfikacja protokołu usuwająca to pole z rekordów aktywacji procedur była by bardzo prosta.
Wskaźnikiem rekordu aktywacji będzie FP zawierający adres pola DL.
wołający 0 ; miejsce na wynik <parametry na stos> adres_wołanego CALL {DROP} ; powtórzone tyle razy, ile jest parametrów ; wynik zostaje na stosie
wołany FP LOAD ; DL na stos SP LOAD FP STORE ; aktualizacja wskaźnika rekordu aktywacji {0} ; powtórzone tyle razy, ile jest zmiennych ... ; tłumaczenie treści funkcji {DROP} ; powtórzone tyle razy, ile jest zmiennych FP STORE ; przywracamy wskaźnik rekordu aktywacji GOTO ; powrót do wołającego
Mechanizm przekazywania parametrów
Dwa najczęściej spotykane tryby przekazywania parametrów to:
- przekazywanie parametru przez wartość
- przekazywanie parametru przez zmienną (przez referencję)
Gdy przekazujemy parametr przez wartość, wołający umieszcza w rekordzie aktywacji wartość argumentu. Wołany może odczytać otrzymaną wartość oraz, jeśli język programowania na to pozwala, zmieniać ją traktując parametr tak samo, jak zmienną lokalną. Ewentualne zmiany nie są jednak widziane przez wołającego.
Przekazanie parametru przez zmienną realizujemy umieszczając w rekordzie aktywacji adres zmiennej. Wołany może odczytać wartość otrzymanego argumentu sięgając pod ten adres, może też pod ten adres coś wpisać, zmieniając tym samym wartość zmiennej będącej argumentem.
Przykład 1
W programie, którego fragment wygląda następująco:
procedure p; var a,b : integer; begin a := q(b,b) end; function q(x : integer; var y : integer) : integer; var z : integer; begin q := x + y; y := 7 end;
rekord aktywacji procedury p wyglądałby tak (w nawiasach podano przesunięcia poszczególnych pól względem pola DL wskazywanego przez rejestr FP):
( 2 ) miejsce na wynik ( 1 ) ślad ( 0 ) DL ( -1 ) a ( -2 ) b
a rekord aktywacji funkcji q tak:
( 4 ) miejsce na wynik ( 3 ) x ( 2 ) y ( 1 ) ślad ( 0 ) DL ( -1 ) z
kod wynikowy dla procedury p miałby postać:
p: FP LOAD ; DL do rekordu aktywacji SP LOAD FP STORE ; nowy adresrekordu aktywacji do FP 0 0 ; rezerwujemy miejsce na dwie zmienne 0 ; miejsce na wynik FP LOAD -2 ADD LOAD ; wartość b FP LOAD -2 ADD ; adres zmiennej b q CALL ; skok ze śladem do q DROP DROP ; usuwamy parametry FP LOAD -1 ADD STORE ; przypisujemy wynik na a DROP DROP ; usuwamy zmienne FP STORE ; wracamy do poprzedniego rekordu aktywacji GOTO ; i do miejsca w kodzie, z którego nas wywołano
a kod funkcji q:
q: FP LOAD ; DL do rekordu aktywacji SP LOAD FP STORE ; nowy adres rekordu aktywacji do FP 0 ; rezerwujemy miejsce na jedną zmienną FP LOAD 3 ADD LOAD ; wartość parametru x na stos FP LOAD 2 ADD LOAD LOAD ; wartość parametru y na stos ADD ; dodajemy x i y FP LOAD 4 ADD ; adres miejsca na wynik STORE ; zapamiętujemy wynik 7 ; stała na stos FP LOAD 2 ADD LOAD ; adres zmiennej przekazanej jako y STORE ; przypisanie na y DROP ; usuwamy zmienną FP STORE ; wracamy do poprzedniego rekordu aktywacji GOTO ; i do miejsca w kodzie, z którego nas wywołano
Środowisko w językach ze strukturą blokową
Wiele języków programowania, między innymi Pascal, pozwala na zagnieżdżanie funkcji. Języki te nazywamy językami ze strukturą blokową. W językach tych kod funkcji ma dostęp nie tylko do jej danych lokalnych, ale także do danych funkcji, w której jest zagnieżdżona itd. w górę hierarchii zagnieżdżenia aż do poziomu globalnego. Działanie funkcji jest więc określone nie tylko przez jej kod oraz parametry, lecz także przez środowisko, w którym ma się wykonać.
Postać środowiska jest w Pascalu wyznaczona statycznie – z kodu programu wynika, do której funkcji należy rekord aktywacji, w którym mamy szukać zmiennej nielokalnej. Mówimy, że w Pascalu obowiązuje statyczne wiązanie zmiennych. Istnieją również języki, w których obowiązuje wiązanie dynamiczne – w przypadku odwołania do danej nielokalnej, szukamy jej w rekordzie aktywacji wołającego itd. w górę po łańcuchu dynamicznym.
Łącze statyczne
By korzystać z danych nielokalnych, działająca funkcja musi mieć dostęp do swojego środowiska. Moglibyśmy przekazać funkcji/procedurze wszystkie dane znajdujące się w jej środowisku jako dodatkowe parametry. Rozwiązanie takie stosuje się często w realizacjach języków funkcyjnych.
W językach proceduralnych i obiektowych z wiązaniem statycznym zmiennych najczęściej stosowanym sposobem reprezentowania środowiska jest powiązanie w listę ciągu rekordów aktywacji funkcji, które są na ścieżce w hierarchii zagnieżdżania. W każdym rekordzie będzie tzw. łącze statyczne (SL, static link) – wskaźnik do jednego z rekordów aktywacji funkcji otaczającej daną. Rekord ten nazywamy poprzednikiem statycznym, a ciąg rekordów połączonych SL to łańcuch statyczny.
SL musi być liczony przez wołającego, bo do jego określenia trzeba znać zarówno funkcję wołaną jak i wołającą. Środowisko dla funkcji wołanej zależy od środowiska wołającej – jeśli obie widzą zmienną „x”, jej wartość ma być dla nich równa. Jeśli funkcja F znajdująca się na poziomie zagnieżdżenia Fp wywołuje G z poziomu zagnieżdżenia Gp, w pole SL wpisze adres rekordu, który odnajdzie przechodząc po własnym łańcuchu statycznym o Fp-Gp+1 kroków w górę. Jeśli np. G jest funkcją lokalną F (Gp=Fp+1), funkcja F w pole SL dla G wpisze adres swojego rekordu; jeśli F i G są na tym samym poziomie zagnieżdżenia, w polu SL dla G będzie to, co w SL dla F itd.
Dostęp do danych nielokalnych
Dane lokalne funkcji są w jej rekordzie aktywacji a dane globalne w ustalonym miejscu w pamięci – można do nich sięgać za pomocą adresów bezwzględnych. Z danych, które nie są ani lokalne ani globalne, korzystamy przy pomocy łańcucha statycznego. W funkcji F o poziomie Fp sięgamy do zmiennej z funkcji G o poziomie Gp (oczywiście Fp>=Gp), przechodząc po łańcuchu statycznym Fp-Gp kroków w górę. Tam pod przesunięciem znanym podczas kompilacji jest nasza zmienna. Adres zmiennej jest więc wyznaczony przez poziom zagnieżdżenia i przesunięcie w rekordzie.
Adresy rekordów z łańcucha statycznego można też wpisać do tablicy (tzw. display). Dzięki temu unikniemy chodzenia po łańcuchu statycznym, ale za to trzeba będzie stale aktualizować tablicę.
Przykład 2
Przyjmijmy, że w rekordzie aktywacji SL będzie się znajdował pomiędzy parametrami a śladem powrotu. Zakładamy też, że protokół wywołania i powrotu z funkcji jest prawie taki sam, jak w poprzednim przykładzie. Jedyną różnicą będzie dodanie po stronie wołającego obliczenia SL i przed wywołaniem i usunięcia go ze stosu po powrocie sterowania.
W programie, którego fragment wygląda następująco:
procedure p; var a : integer; procedure q; var b : integer; procedure r; var c : integer; procedure s; begin ... end {s}; begin a:=b+c; s; q end {r}; begin ... end {q}; begin ... end {p};
rekord aktywacji procedury p wyglądałby tak (w nawiasach podano przesunięcia poszczególnych pól względem pola DL wskazywanego przez rejestr FP):
( 3 ) miejsce na wynik ( 2 ) SL ( 1 ) ślad ( 0 ) DL ( -1 ) a
procedury q tak:
( 3 ) miejsce na wynik ( 2 ) SL ( 1 ) ślad ( 0 ) DL ( -1 ) b
a procedury r tak:
( 3 ) miejsce na wynik ( 2 ) SL ( 1 ) ślad ( 0 ) DL ( -1 ) c
kod wynikowy dla procedury r miałby postać:
r: FP LOAD ; DL do rekordu aktywacji SP LOAD FP STORE ; nowy adresrekordu aktywacji do FP 0 ; rezerwujemy miejsce na zmienną c FP LOAD 2 ADD LOAD ; SL na stos -1 ADD LOAD ; wartość b na stos FP LOAD -1 ADD LOAD ; wartość c na stos ADD ; b+c na stos FP LOAD 2 ADD LOAD ; SL na STOS 2 ADD LOAD ; na stos adres rekordu aktywacji p -1 ADD ; na stos adres zmiennej a STORE ; przypisanie 0 ; miejsce na wynik FP LOAD ; SL dla s na stos s CALL ; skok ze śladem do s DROP ; usuwamy SL DROP ; usuwamy miejsce na wynik 0 ; miejsce na wynik FP LOAD 2 ADD LOAD ; adres rekordu aktywacji q 2 ADD LOAD ; SL dla wywołania q q CALL ; skok ze śladem do q DROP ; usuwamy SL DROP ; usuwamy miejsce na wynik DROP ; usuwamy zmienną c FP STORE ; wracamy do poprzedniego rekordu aktywacji GOTO ; i do miejsca w kodzie, z którego nas wywołano
Realizacja konstrukcji języków obiektowych
W obiektowych językach programowania każdy obiekt posiada pewną wiedzę, przechowywaną na zmiennych instancyjnych (zmiennych obiektowych), ma również określone umiejętności reprezentowane przez metody. To, jakie zmienne instancyjne i jakie metody posiada obiekt danej klasy, wynika z definicji tej klasy oraz z definicji klas, z których dziedziczy bezpośrednio lub pośrednio. W dalszej części wykładu omówimy realizację mechanizmów obiektowości dla języka programowania, w którym każda klasa może dziedziczyć z co najwyżej jednej klasy (języka z pojedynczym dziedziczeniem).
Zmienne instancyjne
Każdy obiekt posiada zmienne zdefiniowane w jego klasie, a także zmienne odziedziczone z nadklas. Reprezentacja obiektów jest analogiczna do rekordów - w obszarze pamięci zajętym przez obiekt znajdują się wartości jego zmiennych instancyjnych. Kolejność tych zmiennych ma być zgodna z hierarchią dziedziczenia - zmienne zdefiniowane w klasie obiektu muszą się znaleźć na końcu, przed nimi są zmienne z klasy dziedziczonej itd. w górę hierarchi dziedziczenia.
Np. w programie zawierającym definicje klas:
class A; var w:integer; procedure piszA; begin write(w) end; end; class B extents A; var x,y:integer; procedure piszB; begin write(w,x,y) end; end; class C extends B; var z:integer; procedure piszC; begin write(w,x,y,z) end; end;
metody "pisz..." wypisują wartości wszystkich zmiennych obiektu danej klasy.
W programie tym, obiekty klasy A będą zawierały jedną zmienną:
w
obiekty klasy B trzy zmienne uporządkowane w kolejności:
w x y
a obiekty klasy C cztery zmienne w kolejności:
w x y z
Przyjęcie takiej właśnie kolejności zmiennych jest konieczne, gdyż chcemy umożliwić metodom zdefiniowanym w danej klasie prawidłowe działanie zarówno dla obiektów tej klasy, jak też wszystkich klas z niej dziedziczących. Musimy więc zagwarantować, by w obiekcie dziedziczącym zmienną znajdowała się ona w tym samym miejscu, co w obiektach klasy dziedziczonej. W naszym przypadku, zarówno w obiektach klasy B jak i klasy C, zmienne x i y są odpowiednio na 2 i 3 pozycji. Metoda piszB wie więc, pod jakim przesunięciem te zmienne się znajdują niezależnie od tego, czy zostanie wywołana dla obiektu klasy B czy C.
Metody wirtualne
Wywołanie metody w językach obiektowych różni się od wywołania funkcji/procedury w językach proceduralnych dwoma elementami.
Po pierwsze, metoda otrzymuje jako dodatkowy ukryty parametr obiekt, dla którego ma sie wykonać. W kodzie metody będziemy się odwoływali do tego parametru za pomocą słowa "self".
Po drugie, w językach obiektowych występuje mechanizm metod wirtualnych, co oznacza, że decyzja o wyborze metody jest podejmowana podczas wykonania programu, a nie podczas kompilacji. Gdy obiekt otrzyma komunikat, reaguje w sposób zależny od swojej klasy. Jeśli w tej klasie jest metoda o nazwie takiej, jak nazwa komunikatu, wywołujemy ją, jeśli nie, to szukamy w nadklasie itd.
Np. w programie zawierającym deklaracje klas:
class A; procedure nazwa; begin write('A') end; procedure pisz; begin write('To jest obiekt klasy '); self.nazwa end; end; class B extends A; procedure nazwa; begin write('B') end; end;
oraz deklarację zmiennej:
var p:A;
wykonanie instrukcji:
p:=create(A); p.pisz
powinno spowodować wypisanie "To jest obiekt klasy A", a wykonanie:
p:=create(B); p.pisz
wypisze "To jest obiekt klasy B".
Bezpośrednia realizacja opisanego powyżej mechanizmu wyboru metody byłaby nieefektywna. Znacznie lepszym i powszechnie stosowanym rozwiązaniem, jest wyposażenie obiektu w tzw. tablicę metod wirtualnych. Znajduje się w niej (dla każdego komunikatu, który obiekt może otrzymać) adres kodu metody, którą należy wykonać.
W językach z typami statycznymi, podczas wykonania programu, obiekt może otrzymać tylko komunikaty odpowiadające metodom zdefiniowanym w jego klasie i metodom dziedziczonym z nadklas. Można więc ponumerować wszystkie komunikaty wysyłane do obiektu i reprezentować tablicę metod wirtualnych za pomocą zwykłej tablicy, w której na K-tej pozycji byłby zapisany adres metody, którą należy wykonać w odpowiedzi na komunikat numer K.
Przy założeniu takiej reprezentacji tablicy metod wirtualnych, wysłanie komunikatu numer K do obiektu będzie wymagało sięgnięcia do tablicy metod wirtualnych tego obiektu na pozycję K-tą, pobrania z niej adresu metody i skoku ze śladem pod ten adres. Pozostałe elementy protokołu wywołania i powrotu będą takie same, jak w przypadku zwykłych funkcji/procedur.
Ponieważ to, jaka metoda obiektu ma się wykonać w odpowiedzi na komunikat, zależy tylko od klasy obiektu, wszystkie obiekty danej klasy mogą mieć wspólną tablicę metod wirtualnych. W samym obiekcie umieszczamy jedynie adres tej tablicy.
Budowa tablic metod wirtualnych oraz numerowanie komunikatów odbywa się podczas kompilacji, na etapie analizy kontekstowej. Tablice metod wirtualnych dla poszczególnych klas budujemy w kolejności przejścia drzewa dziedziczenia „z góry na dół”. Tablica metod wirtualnych dla podklasy powstaje z tablicy dla nadklasy przez dodanie adresów metod zdefiniowanych w tej klasie. Jeśli metoda była już zdefiniowana „wyżej” w hierarchii, czyli jest redefiniowana, jej adres wpisujemy na pozycję metody redefiniowanej. Jeśli metoda pojawia się na ścieżce dziedziczenia pierwszy raz, jej adres wpisujemy na pierwsze wolne miejsce w tablicy metod wirtualnych.
Przykład
Rozważmy następujący fragment programu:
class A; var x:integer; procedure p; begin ... end; procedure q; begin ... end; end; class B extends A; var y:integer; z:A; procedure p; begin y:=x; z.p; self.q end; procedure r; begin ... end; end;
Obiekty klasy A będą miały postać (w nawiasach znajdują się przesunięcia poszczególnych pól):
( 0 ) adres tablicy metod wirtualnych ( 1 ) x
a obiekty klasy B:
( 0 ) adres tablicy metod wirtualnych ( 1 ) x ( 2 ) y ( 3 ) z
Komunikaty wysyłane do obiektów klasy A otrzymają numery 0 (komunikat p) i 1 (komunikat q), a komunikaty wysyłane do obiektów klasy B będą miały numery 0 (komunikat p), 1 (komunikat q) i 2 (komunikat r).
Tablica metod wirtualnych klasy A będzie zawierała:
( 0 ) p z klasy A ( 1 ) q z klasy A
a tablica metod wirtualnych klasy B:
( 0 ) p z klasy B ( 1 ) q z klasy A ( 2 ) r z klasy B
Przyjmijmy też taką samą postać rekordu aktywacji, jak w przykładzie pierwszym, i taki sam protokół wywołania i powrotu, z tą jednak różnicą, że jako dodatkowy, ostatni parametr metody przekazywany jest obiekt, dla którego metoda ma się wykonać, czyli self.
Rekord aktywacji metody p z klasy B będzie miał postać:
( 3 ) miejsce na wynik ( 2 ) self ( 1 ) ślad ( 0 ) DL
Przy tych wszystkich założeniach tłumaczenie metody p z klasy B będzie następujące:
B_p: FP LOAD ; DL na stos SP LOAD FP STORE ; aktualizacja wskaźnika rekordu aktywacji FP LOAD 2 ADD LOAD ; self na stos 1 ADD LOAD ; x na stos FP LOAD 2 ADD LOAD ; self na stos 2 ADD ; adres y na stos STORE ; przypisanie 0 ; miejsce na wynik FP LOAD 2 ADD LOAD ; self na stos 3 ADD LOAD ; z na stos, jako odbiorca komunikatu DUP ; potrzebny także do szukania tablicy metod wirtualnych LOAD ; adres tablicy metod wirtualnych na stos 0 ADD LOAD ; adres metody p jest pod przesunięciem 0 CALL ; skok ze śladem do metody p DROP DROP ; usuwamy self i wynik 0 ; miejsce na wynik FP LOAD 2 ADD LOAD ; self na stos DUP ; potrzebny także do szukania tablicy metod wirtualnych 1 ADD LOAD ; adres metody q jest pod przesunięciem 1 CALL ; skok ze śladem do metody q DROP DROP ; usuwamy self i wynik FP STORE ; przywracamy wskaźnik rekordu aktywacji GOTO ; powrót z metody