Co to jest tłumacz? Języki programowania. Kompilatory i interpretery. Algorytm prostego interpretera

Specyficznymi wykonawcami języków programowania są tłumacze ustni i ustni.

Tłumacz to program, na podstawie którego komputer konwertuje wprowadzone do niego programy na język maszynowy, ponieważ może wykonywać tylko programy napisane w języku swojego procesora, a algorytmy określone w innym języku muszą zostać przetłumaczone na język maszynowy przed ich wykonaniem.

Tłumacz- program lub środek techniczny nadający program.

Emisja programu- przekształcenie programu prezentowanego w jednym z języków programowania na program w innym języku, równoważny pod względem wyników wykonawczych z pierwszym. Tłumacz zwykle diagnozuje także błędy, tworzy słowniki identyfikatorów, przygotowuje teksty programów do druku itp.

Język, w którym prezentowany jest program wejściowy, nazywa się oryginalny język, a sam program - kod źródłowy. Język wyjściowy nazywany jest językiem docelowym lub cel kod. Celem tłumaczenia jest przekształcenie tekstu z jednego języka na inny, zrozumiały dla odbiorcy tekstu. W przypadku programów tłumaczących adresatem jest urządzenie techniczne(procesor) lub program interpretujący.

Tłumacze są implementowane jako kompilatory lub interpretery. Pod względem wykonywania pracy kompilator i interpreter znacznie się różnią.

Język procesorów (kod maszynowy) jest językiem niskiego poziomu. Nazywa się tłumaczem, który konwertuje programy na język maszynowy, który jest odbierany i wykonywany bezpośrednio przez procesor kompilator.

Kompilator(Język angielski) kompilator- kompilator, kolektor) czyta cały program, tłumaczy go i tworzy kompletną wersję programu w języku maszynowym, która następnie jest wykonywana. Wynikiem działania kompilatora jest binarny plik wykonywalny.

Zaleta kompilatora: program jest kompilowany raz i przy każdym jego uruchomieniu nie są wymagane żadne dodatkowe przekształcenia. W związku z tym kompilator nie jest wymagany na maszynie docelowej, dla której program jest kompilowany. Wada: oddzielny etap kompilacji spowalnia pisanie i debugowanie oraz utrudnia uruchamianie małych, prostych lub jednorazowych programów.

Jeżeli językiem źródłowym jest język asemblera (język niskiego poziomu zbliżony do języka maszynowego), wówczas kompilator takiego języka nazywa się monter.

Inną metodą implementacji jest wykonanie programu za pomocą interpretator w ogóle żadnej transmisji.

Interpretator(Język angielski) interpretator- interpreter, interpreter) tłumaczy i wykonuje program linia po linii.

Oprogramowanie interpretera modeluje maszynę, której cykl pobierania i wykonywania działa na podstawie instrukcji w językach wysokiego poziomu, a nie na instrukcjach maszynowych. Ta symulacja oprogramowania tworzy maszynę wirtualną, która implementuje język. To podejście nazywa się czysta interpretacja. Czysta interpretacja jest zwykle stosowana w przypadku języków o prostej strukturze (na przykład APL lub Lisp). Tłumacze wiersz poleceń przetwarzaj polecenia w skryptach w systemie UNIX lub w plikach wsadowych (.bat) w systemie MS-DOS, również zwykle w trybie czystej interpretacji.

Zaleta czystego tłumacza: brak działań pośrednich w tłumaczeniu upraszcza wdrożenie tłumacza i czyni go wygodniejszym w użyciu, także w trybie dialogowym. Wadą jest to, że na maszynie docelowej, na której ma zostać wykonany program, musi znajdować się interpreter. Z reguły występuje mniej lub bardziej znacząca utrata prędkości. A właściwość czystego interpretera polegającą na tym, że błędy w interpretowanym programie są wykrywane dopiero przy próbie wykonania polecenia (lub linii) z błędem, można uznać zarówno za wadę, jak i zaletę.

W implementacji języków programowania istnieją kompromisy między kompilacją a czystą interpretacją, gdy interpreter przed wykonaniem programu tłumaczy go na język pośredni (na przykład na kod bajtowy lub kod p), wygodniejszy do interpretacji (tj. mówimy o tłumaczu z wbudowanym tłumaczem). Ta metoda nazywa się realizacja mieszana. Przykładem implementacji języka mieszanego jest Perl. Podejście to łączy w sobie zarówno zalety kompilatora i interpretera (większa szybkość wykonywania i łatwość użycia), jak i wady (do przetłumaczenia i przechowywania programu w języku pośrednim wymagane są dodatkowe zasoby; do wykonania programu w języku docelowym wymagany jest tłumacz maszyna). Podobnie jak w przypadku kompilatora, implementacja mieszana wymaga tego przed wykonaniem źródło nie zawierała błędów (leksykalnych, składniowych i semantycznych).

Wraz ze wzrostem zasobów komputerów i ekspansją heterogenicznych sieci (w tym Internetu) łączących komputery różnych typów i architektur, pojawił się nowy rodzaj interpretacji, w której kod źródłowy (lub pośredni) jest kompilowany do kodu maszynowego bezpośrednio w czasie wykonywania , "w locie." Już skompilowane sekcje kodu są buforowane, dzięki czemu przy ponownym dostępie do nich natychmiast uzyskują kontrolę, bez konieczności ponownej kompilacji. To podejście nazywa się kompilacja dynamiczna.

Zaletą kompilacji dynamicznej jest to, że szybkość interpretacji programu staje się porównywalna z szybkością wykonywania programu w konwencjonalnych językach kompilowanych, podczas gdy sam program jest przechowywany i dystrybuowany w jednej formie, niezależnie od platform docelowych. Wadą jest większa złożoność implementacji i większe wymagania dotyczące zasobów niż w przypadku prostych kompilatorów lub czystych interpreterów.

Ta metoda sprawdza się dobrze w przypadku aplikacji internetowych. W związku z tym pojawiła się kompilacja dynamiczna, która jest w takim czy innym stopniu obsługiwana w implementacjach Java. NET Framework, Perla, Pythona.

Po skompilowaniu programu do uruchomienia programu nie jest potrzebny ani kod źródłowy programu, ani kompilator. Jednocześnie program przetwarzany przez tłumacza musi zostać ponownie przetłumaczony na język maszynowy przy każdym uruchomieniu programu. Oznacza to, że plik źródłowy jest bezpośrednio wykonywalny.

Skompilowane programy działają szybciej, ale zinterpretowane są łatwiejsze do naprawienia i zmiany.

Każdy konkretny język jest nastawiony albo na kompilację, albo na interpretację – w zależności od celu, dla którego został stworzony. Na przykład C++ jest zwykle używany do rozwiązywania dość złożonych problemów, w których ważna jest szybkość programu, dlatego ten język jest implementowany za pomocą kompilatora.

Aby uzyskać większą szybkość działania programów w interpretowanych językach programowania, można zastosować tłumaczenie na pośredni kod bajtowy. Języki umożliwiające tę sztuczkę to Java, Python i niektóre inne języki programowania.

Algorytm prostego interpretera:

2. przeanalizować instrukcje i określić odpowiednie działania;

3. podjąć odpowiednie działania;

4. jeżeli nie został osiągnięty warunek zakończenia programu, przeczytaj poniższą instrukcję i przejdź do kroku 2

Języki programowania można podzielić na kompilowane i interpretowane.

Program w języku kompilowanym za pomocą specjalnego programu kompilującego jest konwertowany (kompilowany) na zestaw instrukcji dla tego typu procesor (kod maszynowy), a następnie jest zapisywany w wykonywalnym module, który można uruchomić w celu wykonania jako oddzielny program. Innymi słowy, kompilator tłumaczy kod źródłowy programu z języka programowania wysokiego poziomu na kody binarne instrukcji procesora.

Jeśli program jest napisany w języku interpretowanym, wówczas interpreter bezpośrednio wykonuje (interpretuje) tekst źródłowy bez wcześniejszego tłumaczenia. W takim przypadku program pozostaje w języku oryginalnym i nie można go uruchomić bez tłumacza. Można powiedzieć, że procesor komputera jest interpretatorem kodu maszynowego.

Krótko mówiąc, kompilator natychmiast i w całości tłumaczy kod źródłowy programu na język maszynowy, tworząc oddzielny program wykonywalny, a interpreter wykonuje tekst źródłowy podczas działania programu.

Podział na języki kompilowane i interpretowane jest w pewnym stopniu arbitralny. Zatem dla każdego tradycyjnie kompilowanego języka, takiego jak Pascal, możesz napisać interpreter. Ponadto większość współczesnych „czystych” interpreterów nie wykonuje bezpośrednio konstrukcji językowych, ale raczej kompiluje je w jakąś pośrednią reprezentację wysokiego poziomu (na przykład ze zmiennym dereferencją i rozszerzaniem makr).

Kompilator można stworzyć dla dowolnego języka interpretowanego - np. język Lisp, który jest natywnie interpretowany, można kompilować bez żadnych ograniczeń. Kod wygenerowany podczas wykonywania programu może być również dynamicznie kompilowany podczas wykonywania.

Ogólnie rzecz biorąc, skompilowane programy działają szybciej i nie wymagają dodatkowe programy, ponieważ zostały już przetłumaczone na język maszynowy. Jednocześnie za każdym razem, gdy zmieniany jest tekst programu, należy go przekompilować, co stwarza trudności podczas programowania. Ponadto skompilowany program można uruchomić tylko na komputerze tego samego typu i zwykle w tym samym systemie operacyjnym, dla którego kompilator został zaprojektowany. Aby utworzyć plik wykonywalny dla innego typu maszyny, wymagana jest nowa kompilacja.

Języki interpretowane mają pewne specyficzne dodatkowe cechy (patrz wyżej), ponadto programy w nich można uruchamiać natychmiast po modyfikacji, co ułatwia rozwój. Program w języku interpretowanym często można bez dodatkowego wysiłku uruchomić na różnych typach maszyn i systemów operacyjnych.

Jednak interpretowane programy działają zauważalnie wolniej niż skompilowane i nie można ich uruchomić bez dodatkowego programu interpretującego.

Niektóre języki, takie jak Java i C#, dzielą się na języki kompilowane i interpretowane. Mianowicie program nie jest kompilowany do języka maszynowego, ale do kodu niskiego poziomu, niezależnego od maszyny, zwanego kodem bajtowym. Następnie wykonywany jest kod bajtowy maszyna wirtualna. Interpretacja jest zwykle używana do wykonywania kodu bajtowego, chociaż poszczególne jego części można przetłumaczyć na kod maszynowy bezpośrednio podczas wykonywania programu za pomocą kompilacji just-in-time (JIT) w celu przyspieszenia programu. W Javie kod bajtowy jest wykonywany wirtualnie Maszyna Java(Java Virtual Machine, JVM), dla C# — środowisko uruchomieniowe języka wspólnego.

Takie podejście w pewnym sensie pozwala wykorzystać zalety zarówno interpreterów, jak i kompilatorów. Warto również wspomnieć o oryginalnym języku Forth, który posiada zarówno interpreter, jak i kompilator.

Ponieważ tekst napisany w języku programowania jest niezrozumiały dla komputera, należy go przetłumaczyć na kod maszynowy. To tłumaczenie programu z języka programowania na język kodu maszynowego nazywa się tłumaczeniem i wykonują je specjalne programy - tłumacze.

Tłumacz – program usługowy konwertujący program źródłowy podany w wejściowym języku programowania na program pracy, reprezentowany w języku przedmiotowym.

Obecnie tłumaczy dzieli się na trzy główne grupy: asemblery, kompilatory i interpretery.

Asembler to systemowy program narzędziowy, który konwertuje struktury symboliczne na polecenia języka maszynowego. Specyficzną cechą asemblerów jest to, że dokonują one dosłownego tłumaczenia jednej instrukcji symbolicznej na jedną instrukcję maszynową. Zatem język asemblera (zwany także autokodem) ma na celu ułatwienie percepcji systemu poleceń komputera i przyspieszenie programowania w tym systemie poleceń. Programiście znacznie łatwiej jest zapamiętać mnemoniczne oznaczenie instrukcji maszynowych niż ich kod binarny.

Jednocześnie język asemblera, oprócz analogów poleceń maszynowych, zawiera wiele dodatkowych dyrektyw ułatwiających w szczególności zarządzanie zasobami komputera, pisanie powtarzających się fragmentów i budowanie programów wielomodułowych. Dlatego ekspresja języka jest znacznie bogatsza niż tylko symboliczny język kodowania, co znacznie poprawia efektywność programowania.

Kompilator to program usługowy, który tłumaczy program napisany w źródłowym języku programowania na język maszynowy. Podobnie jak asembler, kompilator konwertuje program z jednego języka na inny (najczęściej na język konkretnego komputera). Jednocześnie polecenia w języku źródłowym różnią się znacznie pod względem organizacji i mocy od poleceń w języku maszynowym. Istnieją języki, w których jedno polecenie języka źródłowego jest tłumaczone na 7-10 poleceń maszynowych. Istnieją jednak również języki, w których każde polecenie może mieć 100 lub więcej poleceń maszynowych (na przykład Prolog). Ponadto języki źródłowe dość często stosują ścisłe typowanie danych, przeprowadzane poprzez ich wstępny opis. Programowanie może polegać nie na kodowaniu algorytmu, ale na dokładnym przemyśleniu struktur danych lub klas. Proces tłumaczenia z takich języków nazywany jest zwykle kompilacją, a języki źródłowe zazwyczaj klasyfikowane są jako języki programowania wysokiego poziomu (lub języki wysokiego poziomu). Abstrakcja języka programowania z komputerowego systemu poleceń doprowadziła do niezależnego stworzenia szerokiej gamy języków skupionych na rozwiązywaniu konkretnych problemów. Pojawiły się języki do obliczeń naukowych, obliczeń ekonomicznych, dostępu do baz danych i innych.

Tłumacz - program lub urządzenie, które dokonuje tłumaczenia i wykonywania programu źródłowego przez operatora. W przeciwieństwie do kompilatora, interpreter nie generuje programu w języku maszynowym jako wyniku. Po rozpoznaniu polecenia w języku źródłowym natychmiast je wykonuje. Zarówno kompilatory, jak i interpretery używają tych samych metod analizy kodu źródłowego programu. Ale interpreter pozwala rozpocząć przetwarzanie danych po napisaniu choćby jednego polecenia. Dzięki temu proces tworzenia i debugowania programów jest bardziej elastyczny. Ponadto brak wyjściowego kodu maszynowego pozwala uniknąć bałaganu urządzenia zewnętrzne dodatkowe pliki, a sam interpreter można dość łatwo dostosować do dowolnej architektury maszyny, rozwijając go tylko raz w powszechnie używanym języku programowania. Dlatego powszechne stały się języki interpretowane, takie jak Java Script i VB Script. Wadą interpreterów jest niska prędkość wykonywania programu. Zazwyczaj interpretowane programy działają od 50 do 100 razy wolniej niż programy natywne.

Emulator to program lub narzędzie programowo-sprzętowe, które umożliwia, bez przeprogramowania, wykonanie na danym komputerze programu, który wykorzystuje kody lub metody wykonywania operacji inne niż na danym komputerze. Emulator jest podobny do interpretera w tym sensie, że bezpośrednio wykonuje program napisany w określonym języku. Najczęściej jest to jednak język maszynowy lub kod pośredni. Obydwa reprezentują instrukcje w kodzie binarnym, które można wykonać natychmiast po rozpoznaniu kodu operacji. W przeciwieństwie do programów tekstowych nie ma potrzeby rozpoznawania struktury programu ani wybierania argumentów.

Emulatory są używane dość często do różnych celów. Na przykład podczas opracowywania nowych systemów komputerowych najpierw tworzony jest emulator, który uruchamia programy opracowane dla komputerów, które jeszcze nie istnieją. Pozwala to ocenić system dowodzenia i opracować podstawowe oprogramowanie nawet zanim zostanie stworzony odpowiedni sprzęt.

Bardzo często do uruchamiania starych programów na nowych komputerach używany jest emulator. Zazwyczaj nowsze komputery są szybsze i mają lepsze urządzenia peryferyjne. Pozwala to na skuteczniejszą emulację starszych programów niż uruchamianie ich na starszych komputerach.

Transkoder to program lub urządzenie programowe, które tłumaczy programy napisane w języku maszynowym jednego komputera na programy w języku maszynowym innego komputera. Jeśli emulator jest mniej inteligentnym odpowiednikiem interpretera, wówczas transkoder działa w tej samej roli w stosunku do kompilatora. Podobnie źródłowy (zwykle binarny) kod maszynowy lub reprezentacja pośrednia są konwertowane na inny podobny kod za pomocą jednej instrukcji i bez ogólnej analizy struktury programu źródłowego. Transkodery są przydatne podczas przenoszenia programów z jednej architektury komputera na inną. Można ich również używać do rekonstrukcji tekstu programu w języku wysokiego poziomu z istniejącego kodu binarnego.

Makroprocesor to program, który zastępuje jeden ciąg znaków innym. Jest to rodzaj kompilatora. Generuje tekst wyjściowy poprzez przetwarzanie specjalnych wstawek znajdujących się w tekście źródłowym. Wkładki te są zaprojektowane w specjalny sposób i należą do konstrukcji języka zwanego makrojęzykiem. Makroprocesory są często stosowane jako dodatki do języków programowania, zwiększające funkcjonalność systemów programistycznych. Prawie każdy asembler zawiera makroprocesor, który zwiększa efektywność tworzenia programów maszynowych. Takie systemy programowania nazywane są zwykle makroasemblerami.

Makroprocesory są również używane w językach wysokiego poziomu. Zwiększają funkcjonalność języków takich jak PL/1, C, C++. Makroprocesory są szczególnie szeroko stosowane w językach C i C++, co ułatwia pisanie programów. Makroprocesory poprawiają efektywność programowania bez zmiany składni i semantyki języka.

Składnia to zbiór reguł języka, które określają powstawanie jego elementów. Innymi słowy, jest to zbiór reguł tworzenia semantycznie znaczących ciągów symboli w danym języku. Składnię określa się za pomocą reguł opisujących pojęcia języka. Przykładowe pojęcia to: zmienna, wyrażenie, operator, procedura. Kolejność pojęć i ich dopuszczalne użycie w regułach określa się syntaktycznie prawidłowe struktury, tworzenie programów. To hierarchia obiektów, a nie sposób, w jaki współdziałają ze sobą, jest definiowana za pomocą składni. Na przykład instrukcja może wystąpić tylko w procedurze, wyrażenie w instrukcji, zmienna może składać się z nazwy i opcjonalnych indeksów itp. Składnia nie jest kojarzona z takimi zjawiskami w programie jak „przeskakiwanie do nieistniejącej etykiety” czy „nie zdefiniowana zmienna o podanej nazwie”. To właśnie robi semantyka.

Semantyka - reguły i warunki określające relacje pomiędzy elementami języka i ich znaczeniami semantycznymi, a także interpretacja znaczenia znaczeniowego konstrukcji syntaktycznych języka. Obiekty języka programowania nie tylko są umieszczone w tekście według określonej hierarchii, ale dodatkowo są ze sobą powiązane poprzez inne pojęcia, tworzące rozmaite skojarzenia. Na przykład zmienna, dla której składnia określa prawidłowe położenie tylko w deklaracjach i niektórych instrukcjach, ma określony typ, może być używana z ograniczoną liczbą operacji, ma adres, rozmiar i musi zostać zadeklarowana, zanim będzie mogła być użyte w programie.

Parser to komponent kompilatora, który sprawdza instrukcje źródłowe pod kątem zgodności z regułami składniowymi i semantyką danego języka programowania. Pomimo swojej nazwy analizator sprawdza zarówno składnię, jak i semantykę. Składa się z kilku bloków, z których każdy rozwiązuje własne problemy. Zostanie to omówione bardziej szczegółowo przy opisywaniu struktury tłumacza. programowanie języka kompilatora tłumacza

Każdy tłumacz wykonuje następujące główne zadania:

  • - analizuje przetłumaczony program, w szczególności stwierdza, czy nie zawiera on błędów składniowych;
  • - generuje program wyjściowy (często nazywany programem obiektowym) w języku poleceń maszynowych;
  • - przydziela pamięć dla programu obiektowego. 1.1 Interpretatory

Często cytowaną zaletą implementacji interpretacyjnej jest to, że pozwala ona na „tryb natychmiastowy”. Tryb bezpośredni pozwala zadać komputerowi problem taki jak PRINT 3.14159*3/2.1 i zwrócić odpowiedź natychmiast po naciśnięciu ENTER (pozwala to na użycie komputera o wartości 3000 dolarów jako kalkulatora 10 dolarów). Ponadto interpretery mają specjalne atrybuty, które ułatwiają debugowanie. Można na przykład przerwać przetwarzanie programu interpretującego, wyświetlić zawartość określonych zmiennych, przejrzeć program, a następnie kontynuować wykonywanie.

To, co programiści najbardziej lubią w interpreterach, to możliwość uzyskania szybkiej odpowiedzi. Nie ma tu potrzeby kompilacji, ponieważ interpreter jest zawsze gotowy do ingerencji w Twój program. Wpisz RUN, a wynik będzie Twój Ostatnia zmiana pojawia się na ekranie.

Jednak języki tłumaczone mają wady. Konieczne jest na przykład posiadanie przez cały czas kopii interpretera w pamięci, podczas gdy wiele możliwości interpretera, a co za tym idzie i jego możliwości, może nie być niezbędnych do wykonania konkretnego programu.

Subtelną wadą interpreterów jest to, że mają tendencję do zniechęcania do dobrego stylu programowania. Ponieważ komentarze i inne sformalizowane szczegóły zajmują znaczną ilość pamięci programu, ludzie zwykle z nich nie korzystają. Diabeł jest mniej wściekły niż programista pracujący w interpreterze BASIC, próbujący umieścić program 120 KB w pamięci 60 KB. ale najgorsze jest to, że tłumacze działają powoli.

Spędzają zbyt dużo czasu, próbując wymyślić, co zrobić, zamiast faktycznie wykonać pracę. Wykonując instrukcje programu, interpreter musi najpierw przeskanować każdą instrukcję, aby przeczytać jej zawartość (o co prosi mnie ta osoba?), a następnie wykonać żądaną operację. Operatory w pętlach są skanowane nadmiernie.

Rozważmy program: w interpreterze BASIC 10 FOR N=1 TO 1000 20 PRINT N,SQR(N) 30 NEXT N Kiedy po raz pierwszy przejdziesz przez ten program, interpreter BASIC-a musi dowiedzieć się, co oznacza linia 20:

  • 1. przekonwertuj zmienną numeryczną N na ciąg znaków
  • 2. wyślij ciąg znaków na ekran
  • 3. przejdź do następnego obszaru wydruku
  • 4. oblicz pierwiastek kwadratowy z N
  • 5. przekonwertuj wynik na ciąg znaków
  • 6. wyślij ciąg znaków na ekran

Podczas drugiego przebiegu cyklu całe to rozwiązanie jest powtarzane ponownie, ponieważ wszystkie wyniki badania tej prostej kilka milisekund temu zostały całkowicie zapomniane. I tak przez wszystkie następne 998 przebiegów. Oczywiście, gdybyś był w stanie w jakiś sposób oddzielić fazę skanowania/rozumienia od fazy wykonania, miałbyś ich więcej szybki program. I właśnie do tego służą kompilatory.


4. Podstawowe zasady konstruowania tłumaczy. Tłumacze, kompilatory i interpretery - ogólny schemat pracy. Nowoczesne kompilatory i interpretery.

Podstawowe zasady konstruowania tłumaczy.

Tłumacze, kompilatory, interpretery - ogólny schemat pracy.

Definicja tłumacza, kompilatora, tłumacza

Na początek podajmy kilka definicji - czym właściwie są wielokrotnie wspominani już tłumacze i kompilatory.

Formalna definicja tłumacza

Tłumacz to program, który tłumaczy program wejściowy w języku źródłowym (wejściowym) na równoważny program wyjściowy w języku wynikowym (wyjściowym). W tej definicji słowo „program” pojawia się trzykrotnie i nie jest to błąd ani tautologia. Tak naprawdę w pracę tłumacza zaangażowane są zawsze trzy programy.

Po pierwsze, sam tłumacz jest programem 1 - zwykle jest częścią oprogramowania systemowego systemu komputerowego. Oznacza to, że tłumacz to oprogramowanie; jest to zestaw instrukcji maszynowych i danych, wykonywany przez komputer, podobnie jak wszystkie inne programy w systemie operacyjnym (OS). Wszystkie komponenty tłumacza to fragmenty programu lub moduły z własnymi danymi wejściowymi i wyjściowymi.

Po drugie, danymi wejściowymi dla tłumacza jest tekst programu wejściowego – pewna sekwencja zdań w wejściowym języku programowania. Zwykle jest to plik symboli, ale plik musi zawierać tekst programu spełniający wymagania składniowe i semantyczne języka wejściowego. Ponadto plik ten niesie ze sobą pewne znaczenie określone przez semantykę języka wejściowego.

Po trzecie, wyjściem tłumacza jest tekst powstałego programu. Powstały program jest budowany zgodnie z regułami syntaktycznymi określonymi w języku wyjściowym tłumacza, a jego znaczenie zależy od semantyki języka wyjściowego. Ważnym wymogiem przy definiowaniu tłumacza jest równoważność programów wejściowych i wyjściowych. Równoważność dwóch programów oznacza zbieżność ich znaczenia z punktu widzenia semantyki języka wejściowego (dla programu źródłowego) i semantyki języka wyjściowego (dla programu wynikowego). Bez spełnienia tego wymogu sam tłumacz traci wszelkie praktyczne znaczenie.

Aby więc utworzyć tłumacza, musisz najpierw wybrać języki wejściowe i wyjściowe. Z punktu widzenia przekształcenia zdań języka wejściowego na zdania równoważne języka wyjściowego, tłumacz pełni rolę tłumacza. Na przykład tłumaczenie programu z języka C na język asemblera zasadniczo nie różni się od tłumaczenia, powiedzmy, z rosyjskiego na angielski, z tą tylko różnicą, że złożoność języków jest nieco inna (dlaczego nie ma tłumaczy z języka naturalnego języki – patrz rozdział „Klasyfikacja” języków i gramatyk”, rozdział 9). Dlatego samo słowo „tłumacz” (angielski: tłumacz) oznacza „tłumacz”.

Efektem pracy tłumacza będzie powstały program, ale tylko wtedy, gdy tekst programu źródłowego jest poprawny – nie zawiera błędów w zakresie składni i semantyki języka wejściowego. Jeżeli program źródłowy jest niepoprawny (zawiera co najmniej jeden błąd), wówczas efektem pracy tłumacza będzie komunikat o błędzie (zwykle z dodatkowymi wyjaśnieniami i wskazaniem miejsca wystąpienia błędu w programie źródłowym). W tym sensie tłumacz przypomina tłumacza na przykład z języka angielskiego, któremu podano zły tekst.

Teoretycznie możliwe jest zaimplementowanie tłumacza przy użyciu sprzętu. Autor spotkał się z takimi osiągnięciami, ale ich powszechne zastosowanie praktyczne nie jest znane. W takim przypadku wszystkie podzespoły tłumacza można zaimplementować w postaci sprzętu i ich fragmentów – wtedy układ rozpoznawania może otrzymać całkowicie praktyczną implementację!

Definicja kompilatora.

Różnica między kompilatorem a tłumaczem

Oprócz pojęcia „tłumacz” szeroko stosowane jest również pojęcie „kompilatora”, które ma podobne znaczenie.

Kompilator - Jest to tłumacz, który tłumaczy program źródłowy na równoważny program obiektowy w języku poleceń maszynowych lub języku asemblera.

Zatem kompilator różni się od tłumacza tylko tym, że powstały program musi zawsze być napisany w kodzie maszynowym lub języku asemblera. Powstały program tłumaczący ogólnie można napisać w dowolnym języku - możliwy jest na przykład tłumacz programów z Pascala na C. W związku z tym każdy kompilator jest tłumaczem, ale nie odwrotnie - nie każdy tłumacz będzie kompilatorem. Przykładowo wspomniany powyżej tłumacz Pascal na C nie będzie kompilatorem 1 .

Samo słowo „kompilator” pochodzi od Termin angielski„kompilator” („kompilator”, „linker”). Najwyraźniej termin swoje pochodzenie zawdzięcza zdolności kompilatorów do tworzenia programów obiektowych w oparciu o programy źródłowe.

Powstały program kompilatora nazywany jest „programem obiektowym” lub „kodem wynikowym”. Plik, w którym jest zapisany, nazywany jest zwykle „plikiem obiektowym”. Nawet jeśli wynikowy program jest generowany w języku poleceń maszynowych, istnieje znacząca różnica pomiędzy programem obiektowym (plikiem obiektowym) a programem wykonywalnym (plikiem wykonywalnym). Program wygenerowany przez kompilator nie może być bezpośrednio wykonany na komputerze, gdyż nie jest powiązany z konkretnym obszarem pamięci, w którym powinien znajdować się jego kod i dane (więcej szczegółów w podrozdziale „Zasady funkcjonowania systemów programistycznych”, rozdz. 15) 2.

Kompilatory są oczywiście najpopularniejszym rodzajem tłumaczy (wiele osób uważa je za jedyny rodzaj tłumaczy, choć nie jest to prawdą). Mają najszersze zastosowanie praktyczne, co wynika z powszechnego stosowania wszelkiego rodzaju języków programowania. W dalszej części zawsze będziemy mówić o kompilatorach, co oznacza, że ​​program wyjściowy jest napisany w nich

Naturalnie, tłumacze i kompilatory, podobnie jak wszystkie inne programy, są opracowywane przez osobę (ludzie) - zwykle grupę programistów. W zasadzie mogliby stworzyć go bezpośrednio w języku poleceń maszynowych, jednak ilość kodu i danych współczesnych kompilatorów jest taka, że ​​utworzenie ich w języku poleceń maszynowych jest praktycznie niemożliwe w rozsądnym czasie i przy rozsądnych kosztach pracy. Dlatego prawie wszystkie współczesne kompilatory są również tworzone przy użyciu kompilatorów (zwykle tę rolę pełnią poprzednie wersje kompilatorów tego samego producenta). I w tej roli kompilator jest już programem wyjściowym dla innego kompilatora, który nie jest ani lepszy, ani gorszy od wszystkich innych wygenerowanych programów wyjściowych 2.

Definicja tłumacza. Różnica między tłumaczami ustnymi i pisemnymi

Oprócz podobnych pojęć „tłumacz” i „kompilator”, istnieje zasadniczo odmienna koncepcja interpretera.

Tłumacz - jest to program, który pobiera program wejściowy w języku źródłowym i wykonuje go.

W przeciwieństwie do tłumaczy, interpretery nie generują wynikowego programu (ani w ogóle żadnego wynikowego kodu) - i to jest podstawowa różnica między nimi. Tłumacz, podobnie jak tłumacz, analizuje tekst programu źródłowego. Nie generuje jednak powstałego programu, ale natychmiast wykonuje oryginalny, zgodnie z jego znaczeniem, nadanym przez semantykę języka wejściowego. Zatem wynikiem interpretera będzie wynik określony w rozumieniu programu źródłowego, jeśli ten program jest poprawny, lub komunikat o błędzie, jeśli program źródłowy jest niepoprawny.

Oczywiście, aby wykonać program źródłowy, interpreter musi w jakiś sposób przekonwertować go na język kodu maszynowego, ponieważ w przeciwnym razie nie byłoby możliwe wykonanie programów na komputerze. Robi to, lecz powstałe kody maszynowe nie są dostępne – nie są one widoczne dla użytkownika interpretera. Te kody maszynowe są generowane przez interpreter, wykonywane i niszczone.

1 Należy szczególnie wspomnieć, że obecnie we współczesnych systemach programowania zaczęły pojawiać się kompilatory, w których powstały program tworzony jest nie w języku poleceń maszynowych czy asemblerze, ale w jakimś języku pośrednim. Sam ten język pośredni nie może być wykonywany bezpośrednio na komputerze, ale wymaga specjalnego pośredniego interpretera do wykonywania napisanych na nim programów. Chociaż w tym przypadku prawdopodobnie bardziej poprawne byłoby określenie „tłumacz”, w literaturze używa się terminu „kompilator”, ponieważ język pośredni jest językiem bardzo niskiego poziomu, zbliżonym do instrukcji maszynowych i języków asemblera.

W tym miejscu pojawia się odwieczne pytanie „kura i jajko”. Oczywiście w pierwszej generacji pierwsze kompilatory pisano bezpośrednio na instrukcjach maszynowych, ale potem, wraz z pojawieniem się kompilatorów, odeszli od tej praktyki. Nawet najbardziej krytyczne części kompilatorów są tworzone przynajmniej przy użyciu języka asemblera - i to również jest przetwarzane przez kompilator. są dostosowywane w zależności od potrzeb - zgodnie z wymaganiami konkretnej implementacji) interpretera. Użytkownik widzi wynik wykonania tych kodów - jest to wynik wykonania programu źródłowego (wymóg równoważności programu źródłowego i wygenerowanych kodów maszynowych w tym przypadku (warunkowo musi być spełniony).

Zagadnienia związane z implementacją interpreterów i ich różnicą w stosunku do kompilatorów omówiono bardziej szczegółowo w dalszej części odpowiedniej sekcji.

Cel tłumaczy, kompilatorów i tłumaczy ustnych. Przykłady realizacji

Pierwsze programy, które powstały dla komputerów pierwszej generacji, pisane były bezpośrednio w języku kodu maszynowego. To była naprawdę piekielna robota. Od razu stało się jasne, że nikt nie powinien i nie może mówić językiem poleceń maszynowych, nawet jeśli jest informatykiem. O; Jednakże wszelkie próby nauczenia komputera mówienia ludzkimi językami zakończyły się sukcesem i jest mało prawdopodobne, że kiedykolwiek się powiedzie (z pewnych pozytywnych powodów omówionych w pierwszym rozdziale tego podręcznika).

Od tego czasu cały rozwój oprogramowania komputerowego jest nierozerwalnie związany z pojawieniem się i rozwojem kompilatorów.

Pierwszymi kompilatorami były kompilatory z języków asemblera lub, jak je nazywano, kodów mnemonicznych. Kody mnemoniczne zamieniły „gramatykę Philkina” języka poleceń maszynowych w język monicznych (głównie angielskich) zapisów tych poleceń, mniej lub bardziej zrozumiałych dla specjalisty. (tworzenie programów stało się już znacznie łatwiejsze, ale żaden komputer nie jest w stanie sam wykonać mnems (języka asemblera); w związku z tym pojawiła się potrzeba stworzenia kompilatorów. Kompilatory te są podstawowe, ale nadal odgrywają znaczącą rolę w programowaniu systemy dzisiaj Więcej szczegółów na temat języka asemblera i kompilatorów znajduje się w artykule na ten temat: dalej w odpowiednim dziale.

Kolejnym etapem było stworzenie języków wysokiego poziomu. Języki wysokiego poziomu (do nich zalicza się większość języków programowania) stanowią pewnego rodzaju ogniwo pośrednie pomiędzy językami czysto formalnymi a językami naturalnej komunikacji między ludźmi. Od pierwszego otrzymali ścisłą malizację składniowej struktury zdań języka, od drugiego - znaczną część słownictwa, semantykę podstawowych konstrukcji i wyrażeń (z elementami operacji matematycznych wywodzącymi się z algebry).

Pojawienie się języków wysokiego poziomu znacząco uprościło proces programowania, choć nie sprowadziło go do „poziomu gospodyni domowej”, jak arogancko twierdzili niektórzy autorzy u zarania języków programowania1. Takich języków było tylko kilka, potem dziesiątki, a obecnie jest ich prawdopodobnie ponad setka. Nie widać końca tego procesu. Niemniej jednak w dalszym ciągu dominują komputery o tradycyjnej architekturze „Neumann”, które rozumieją jedynie instrukcje maszynowe, dlatego kwestia tworzenia kompilatorów jest nadal aktualna.

Gdy tylko pojawiła się ogromna potrzeba tworzenia kompilatorów, zaczęła się rozwijać specjalistyczna teoria. Z biegiem czasu znalazł praktyczne zastosowanie w wielu tworzonych kompilatorach. Kompilatory były i nadal są tworzone nie tylko dla nowych, ale także dla języków już dawno znanych. Wielu producentów, od znanych, renomowanych firm (takich jak Microsoft czy Inprise) po mało znane zespoły autorskie, wypuszcza na rynek coraz to nowe próbki kompilatorów. Dzieje się tak z kilku powodów, które zostaną omówione poniżej.

Wreszcie, ponieważ większość teoretycznych aspektów w dziedzinie kompilatorów została wdrożona w praktyce (a to, trzeba powiedzieć, nastąpiło dość szybko, pod koniec lat 60.), rozwój kompilatorów podążał ścieżką ich przyjazności dla człowieka - użytkownik, twórca programów w językach wysokiego poziomu. Logicznym zakończeniem tego procesu było stworzenie systemów programistycznych - systemy oprogramowania, które oprócz samych kompilatorów łączą wiele powiązanych komponentów oprogramowania. Po pojawieniu się systemy programistyczne szybko podbiły rynek i obecnie w większości go dominują (w rzeczywistości samodzielne kompilatory są rzadkością wśród nowoczesnych narzędzi programowych). Informacje o tym, czym są współczesne systemy programowania i jak są zorganizowane, można znaleźć w rozdziale „Nowoczesne systemy programowania”. Obecnie kompilatory są integralną częścią każdego systemu komputerowego. Bez nich zaprogramowanie dowolnego zadania byłoby trudne, jeśli nie po prostu niemożliwe. A programowanie specjalizowanych zadań systemowych z reguły odbywa się jeśli nie w języku wysokiego poziomu (w tej roli obecnie najczęściej używany jest język C), to w języku asemblera, dlatego stosuje się odpowiedni kompilator. Programowanie bezpośrednio w językach kodu maszynowego zdarza się niezwykle rzadko i tylko w celu rozwiązania bardzo wąskich problemów. Kilka słów o przykładowych implementacjach kompilatorów i interpreterów oraz o ich powiązaniu z innymi już istniejącymi oprogramowanie. Kompilatory, jak zostanie pokazane później, są zwykle nieco prostsze w implementacji niż interpretery. Charakteryzują się także wyższą wydajnością - oczywiste jest, że skompilowany kod zawsze zostanie wykonany szybciej niż interpretacja podobnego programu źródłowego. Poza tym nie każdy język programowania pozwala na zbudowanie prostego interpretera. Interpretery mają jednak jedną istotną zaletę – skompilowany kod jest zawsze powiązany z architekturą systemu komputerowego, na który jest przeznaczony, a program źródłowy jedynie z semantyką języka programowania, który jest znacznie łatwiejszy do standaryzacji. Początkowo ten aspekt nie był brany pod uwagę. Pierwszymi kompilatorami były kompilatory kodu mnemonicznego. Ich potomkowie - nowoczesne kompilatory z języków asemblerowych - istnieją dla prawie wszystkich znanych systemów komputerowych. Są niezwykle zorientowani architektonicznie. Następnie pojawiły się kompilatory z języków takich jak FORTRAN, ALGOL-68, PL/1. Były skierowane do dużych komputerów z wsadowym przetwarzaniem zadań. Z powyższego być może tylko FORTRAN jest nadal używany, ponieważ ma ogromną liczbę bibliotek do różnych celów. Wiele języków, które się narodziło, nigdy nie rozpowszechniło się - ADA, Modula, Simula są znane tylko wąskiemu kręgowi specjalistów. W tym samym czasie na rynku systemy oprogramowania zdominowany przez kompilatory języków, które nie mają przed sobą świetlanej przyszłości. Po pierwsze, teraz jest to C i C++. Pierwszy z nich urodził się z system operacyjny typu UNIX, wraz z nim zdobył swoje „miejsce pod słońcem”, a następnie przeniósł się do innych typów systemów operacyjnych. Drugi z powodzeniem ucieleśniał przykład wdrożenia pomysłów programowania obiektowego na dobrze sprawdzonych podstawach praktycznych 1. Można też wspomnieć o dość rozpowszechnionym Pascalu, który niespodziewanie dla wielu wyszedł poza zakres języka czysto edukacyjnego dla środowiska uniwersyteckiego.

Historia tłumaczy ustnych nie jest tak bogata (jeszcze!). Jak już wspomniano, początkowo nie przywiązywano do nich większego znaczenia, ponieważ prawie pod każdym względem są gorsze od kompilatorów. Spośród dobrze znanych języków wymagających interpretacji można wymienić tylko Basic, chociaż większość ludzi zna teraz jego skompilowaną implementację Visual Basic, stworzoną przez Microsoft. Jednak teraz sytuacja nieco się zmieniła, ponieważ kwestia przenośności programów i ich niezależności od platformy sprzętowej staje się coraz bardziej istotna wraz z rozwojem Internetu. Najbardziej znanym obecnie przykładem jest język Java (sam łączy w sobie kompilację i interpretację) i powiązany z nim JavaScript. Przecież język HTML, na którym oparty jest protokół HTTP, który dał impuls do tak szybkiego rozwoju sieci WWW, jest także językiem interpretowanym. Zdaniem autora na wszystkich wciąż czekają niespodzianki w dziedzinie pojawienia się nowych interpreterów, a pierwsze z nich już się pojawiły - na przykład język C# („C-sharp”, ale nazwa wszędzie brzmi „ C Sharp”), ogłoszony przez Microsoft.

O O historii języków programowania i obecnym stanie rynku kompilatorów można mówić długo. Autor uważa, że ​​można ograniczyć się do tego, co już zostało powiedziane, ponieważ nie jest to celem tego podręcznika. Zainteresowani mogą sięgnąć do literatury.

Etapy transmisji. Ogólny schemat działania tłumacza

Na ryc. 13.1 pokazuje ogólny schemat kompilatora. Wynika z tego jasno, że ъ Ogólnie rzecz biorąc, proces kompilacji składa się z dwóch głównych etapów - syntezy i analizy.

Na etapie analizy rozpoznawany jest tekst programu źródłowego oraz tworzone i wypełniane są tabele identyfikatorów. Efektem jego pracy jest pewna wewnętrzna reprezentacja programu, zrozumiała dla kompilatora.

Na etapie syntezy, w oparciu o wewnętrzną reprezentację programu oraz informacje zawarte w tabeli (tabelach) identyfikatorów, generowany jest tekst powstałego programu. Wynikiem tego etapu jest kod wynikowy.

Dodatkowo w kompilatorze znajduje się część odpowiedzialna za analizę i poprawianie błędów, która w przypadku wystąpienia błędu w tekście programu źródłowego powinna możliwie najdokładniej poinformować użytkownika o rodzaju błędu i miejscu jego wystąpienia. W najlepszym razie kompilator może zaoferować użytkownikowi opcję poprawienia błędu.

Etapy te z kolei składają się z mniejszych etapów zwanych fazami kompilacji. Skład faz kompilacji jest podany w najbardziej ogólnej formie, ich specyficzna realizacja i proces interakcji

Po pierwsze, jest to funkcja rozpoznawania języka programu źródłowego. Następnie musi otrzymać na wejściu łańcuch symboli języka wejściowego, sprawdzić, czy należy on do tego języka, a ponadto zidentyfikować reguły, według których ten łańcuch był zbudowany (bo odpowiedź na pytanie o przynależność „tak” i „nie” ” jest mało interesujące). Co ciekawe, generatorem łańcuchów języków wejściowych jest użytkownik – autor programu wejściowego.

Po drugie, kompilator jest generatorem języka wynikowego programu. Musi skonstruować na wyjściu łańcuch języka wyjściowego według Opry; zasady leary, zamierzony język lub język instrukcji maszynowej I próbnik. Elementem rozpoznającym ten łańcuch będzie system komputerowy, dla którego tworzony jest powstały program.

Analiza leksykalna(skaner) to część kompilatora, która czyta program w języku źródłowym i konstruuje z nich słowa (tokeny) języka źródłowego. Danem wejściowym analizatora leksykalnego jest tekst programu źródłowego, a informacja wyjściowa przekazywana jest do dalszego przetwarzania do kompilatora na etapie analizowania. Z teoretycznego punktu widzenia analizator leksykalny nie jest obowiązkową, niezbędną częścią kompilatora. Istnieją jednak powody, które decydują o jego obecności w prawie wszystkich kompilatorach. Więcej szczegółów znajdziesz w sekcji „Analizatory leksykalne”. Zasady budowy skanerów.”

Rozbiór gramatyczny zdania- To jest główna część kompilatora na etapie analizy. O dokonuje selekcji struktur syntaktycznych w tekście programu źródłowego, przetwarzanych przez analizator leksykalny. W tej samej fazie kompilator sprawdza poprawność składniową programu. Główną rolę odgrywa parser syntaktyczny - rolę rozpoznawania tekstu wejściowego języka programowania (patrz sekcja „Parsery. Tłumaczenie kontrolowane syntaktycznie” w tym rozdziale).

Analiza semantyczna- jest to część kompilatora, która sprawdza poprawność* tekstu programu źródłowego pod kątem semantyki języka wejściowego. KRS bezpośrednio sprawdza, analiza semantyczna musi przeprowadzić konwersję; Nazwy tekstowe wymagane przez semantykę języka wejściowego (takie jak dodanie niejawnych funkcji konwersji typów). W różnych implementacjach comp. Tors, analizę semantyczną można częściowo włączyć do fazy analizowania składni*, a częściowo do fazy przygotowania do wygenerowania kodu.

Przygotowanie do generowania kodu- jest to faza, w której kompilator wykonuje czynności wstępne, związane bezpośrednio z syntezą powstałego tekstu programu, ale nie prowadzące jeszcze do wygenerowania tekstu w języku obcym. Zazwyczaj ta faza obejmuje działania związane z identyfikacją elementów języka, alokacją pamięci itp. (patrz sekcja „Analiza semantyczna i przygotowanie do generowania kodu”, rozdział 14).

Generowanie kodu- jest to faza bezpośrednio związana z powstaniem przecinka składników zdania języka docelowego i powstałego tekstu jako całości

Może się oczywiście różnić w zależności od wersji kompilatora. Jednak w takiej czy innej formie wszystkie przedstawione fazy są prawie zawsze obecne w każdym konkretnym kompilatorze.

Kompilator jako całość, z punktu widzenia teorii języków formalnych, występuje w „dwóch postaciach” i spełnia dwie główne funkcje. programy. Jest to główna faza fazy syntezy powstałego programu. Oprócz bezpośredniego wygenerowania tekstu powstałego programu, generowanie zwykle obejmuje także optymalizację – proces związany z przetwarzaniem już wygenerowanego tekstu. Czasami optymalizacja jest wydzielona na osobną fazę kompilacji, ponieważ ma ona istotny wpływ na jakość i wydajność powstałego programu (patrz sekcje „Generowanie kodu. Metody generowania kodu” i „Optymalizacja kodu. Podstawowe metody optymalizacji”, rozdział 14).

Tabele identyfikatorów(czasami nazywane „tablicami symboli”) to specjalnie zorganizowane zbiory danych, które służą do przechowywania informacji o elementach programu źródłowego, które następnie służą do generowania tekstu wynikowego programu. W konkretnej implementacji kompilatora może istnieć jedna tabela identyfikatorów lub może istnieć kilka takich tabel. Elementami programu źródłowego, o których informacja musi zostać zapamiętana w procesie kompilacji, są zmienne, stałe, funkcje itp. - konkretny skład zbioru elementów zależy od użytego wejściowego języka programowania. Koncepcja „tabeli” wcale nie oznacza, że ​​przechowywanie tych danych powinno być zorganizowane właśnie w formie tabel lub innych tablic informacyjnych – możliwe sposoby ich uporządkowania omówiono szczegółowo w dalszej części rozdziału „Tabele identyfikatorów. Organizacja tabel identyfikatorów.”

Pokazane na ryc. 13.1, podzielenie procesu kompilacji na fazy służy raczej celom metodologicznym i w praktyce może nie być tak rygorystycznie przestrzegane. W dalszej części tego podręcznika rozważono różne opcje organizacji technicznej przedstawionych faz kompilacji. Wskazano także, w jaki sposób mogą być one ze sobą powiązane. Tutaj rozważymy jedynie ogólne aspekty tego rodzaju relacji.

Po pierwsze, w fazie analizy leksykalnej z tekstu programu wejściowego wyodrębniane są leksemy, o ile są one niezbędne w następnej fazie analizowania. Po drugie, jak zostanie pokazane poniżej, parsowanie i generowanie kodu mogą być wykonywane jednocześnie. Zatem te trzy fazy kompilacji mogą działać w połączeniu, a wraz z nimi można również przeprowadzić przygotowanie do generowania kodu. Następnie rozważamy kwestie techniczne realizacji głównych faz kompilacji, które są ściśle powiązane z koncepcją przejście.

Koncepcja przejścia. Kompilatory wieloprzebiegowe i jednoprzebiegowe

Jak już wspomniano, proces kompilacji programów składa się z kilku etapów. W prawdziwych kompilatorach skład tych faz może nieznacznie różnić się od omówionego powyżej - niektóre z nich można podzielić na komponenty, inne wręcz przeciwnie, łączą się w jedną fazę. Kolejność wykonywania faz kompilacji może również różnić się w zależności od wariantu kompilatora. W jednym przypadku kompilator przegląda tekst programu źródłowego, natychmiast wykonuje wszystkie fazy kompilacji i otrzymuje wynik - kod obiektowy. W innym wariancie wykonuje tylko część faz kompilacji na tekście źródłowym i otrzymuje nie wynik końcowy, ale zestaw niektórych danych pośrednich. Dane te są następnie ponownie przetwarzane, a proces ten można powtórzyć kilka razy.

Prawdziwe kompilatory z reguły wykonują tłumaczenie tekstu programu źródłowego w kilku przejściach.

Przejście - Jest to proces sekwencyjnego odczytywania przez kompilator danych z pamięci zewnętrznej, przetwarzania ich i umieszczania wyniku pracy w pamięci zewnętrznej. Najczęściej pojedynczy przebieg obejmuje wykonanie jednej lub więcej faz kompilacji. Wynikiem przejść pośrednich jest wewnętrzna reprezentacja programu źródłowego, wynikiem ostatniego przebiegu jest wynikowy program obiektowy.

Jak pamięć zewnętrzna Można używać dowolnych nośników danych - pamięci RAM komputera, napędów dysków magnetycznych, taśm magnetycznych itp. Nowoczesne kompilatory z reguły starają się maksymalnie wykorzystać pamięć RAM komputera do przechowywania danych i tylko wtedy, gdy brakuje dostępnej pamięci, twarde dyski magnetyczne są używanymi dyskami. Inne nośniki danych nie są używane we współczesnych kompilatorach ze względu na niską prędkość wymiany danych.

Po wykonaniu każdego przebiegu kompilator ma dostęp do informacji uzyskanych ze wszystkich poprzednich przebiegów. Z reguły wykorzystuje przede wszystkim informacje uzyskane z przebiegu bezpośrednio poprzedzającego bieżący, ale w zasadzie może uzyskać dostęp do danych z wcześniejszych przebiegów, aż do kodu źródłowego programu. Informacje uzyskane przez kompilator podczas wykonywania przebiegów nie są dostępne dla użytkownika. Jest albo przechowywany w pamięć o dostępie swobodnym, który jest wydawany przez kompilator po zakończeniu procesu tłumaczenia lub jest formatowany jako pliki tymczasowe na dysku, które również ulegają zniszczeniu po zakończeniu pracy kompilatora. Dlatego osoba pracująca z kompilatorem może nawet nie wiedzieć, ile przebiegów wykonuje kompilator - zawsze widzi tylko tekst programu źródłowego i wynikowy program obiektowy. Ważna jest jednak liczba wykonanych podań Specyfikacja techniczna kompilator, renomowane firmy - twórcy kompilatorów zwykle wskazują to w opisie swojego produktu.

Oczywiste jest, że programiści starają się minimalizować liczbę przebiegów wykonywanych przez kompilatory. Zwiększa to szybkość kompilatora i zmniejsza ilość potrzebnej pamięci. Idealny jest kompilator jednoprzebiegowy, który pobiera program źródłowy jako dane wejściowe i natychmiast tworzy wynikowy program obiektowy.

Nie zawsze jednak można zmniejszyć liczbę przejść. O liczbie wymaganych zaliczeń decydują przede wszystkim reguły gramatyczne i semantyczne języka źródłowego. Im bardziej złożona gramatyka języka i im więcej opcji sugerują reguły semantyczne, tym więcej przebiegów wykona kompilator (oczywiście rolę odgrywają także kwalifikacje twórców kompilatora). Na przykład dlatego kompilatory z języka Pascal zwykle działają szybciej niż kompilatory z języka C - gramatyka Pascala jest prostsza, a reguły semantyczne bardziej rygorystyczne. Kompilatory jednoprzebiegowe są rzadkie i możliwe tylko dla bardzo prostych języków. Prawdziwe kompilatory zazwyczaj wykonują od dwóch do pięciu przebiegów. Zatem prawdziwe kompilatory są wieloprzebiegowe. Najpopularniejsze są kompilatory dwu- i trójprzebiegowe, na przykład: pierwszy przebieg to analiza leksykalna, drugi to parsowanie i analiza semantyczna, trzeci to generowanie i optymalizacja kodu (opcje implementacji zależą oczywiście od programisty). We współczesnych systemach programowania pierwszy przebieg kompilatora (analiza leksykalna kodu) często wykonywany jest równolegle z edycją kodu programu źródłowego (ta opcja konstruowania kompilatorów została omówiona w dalszej części tego rozdziału).

Tłumacze. Cechy konstruowania tłumaczy

Interpretator to program, który przyjmuje program wejściowy w języku źródłowym i wykonuje go. Jak wspomniano powyżej, główna różnica między interpreterami a tłumaczami i kompilatorami polega na tym, że interpreter nie generuje programu wynikowego, ale po prostu wykonuje oryginalny program.

Termin „tłumacz”, podobnie jak „tłumacz”, oznacza „tłumacz”. Z punktu widzenia terminologii pojęcia te są podobne, jednak z punktu widzenia teorii języków formalnych i kompilacji istnieje między nimi duża, zasadnicza różnica. Jeśli pojęcia „tłumacz” i „kompilator” są prawie nie do odróżnienia, to nie można ich mylić z pojęciem „tłumacza”.

Najprostszym sposobem wdrożenia interpretera byłoby całkowite przetłumaczenie programu źródłowego na instrukcje maszynowe, a następnie natychmiastowe wykonanie. W takiej implementacji interpreter w rzeczywistości niewiele różniłby się od kompilatora, z tą tylko różnicą, że wynikowy program nie byłby dostępny dla użytkownika. Wadą takiego interpretera byłoby to, że użytkownik musiałby poczekać, aż cały program źródłowy zostanie skompilowany, zanim będzie można rozpocząć wykonywanie. Tak naprawdę taki interpreter nie miałby specjalnego sensu - nie zapewniałby żadnej przewagi nad podobnym kompilatorem 1 . Dlatego zdecydowana większość interpreterów działa w taki sposób, że wykonują program źródłowy sekwencyjnie, gdy tylko dotrze on do wejścia interpretera. Użytkownik nie musi wtedy czekać na skompilowanie całego programu źródłowego. Co więcej, może on sekwencyjnie wprowadzać oryginalny program i natychmiast obserwować wynik jego wykonania po wprowadzeniu poleceń.

Przy takiej kolejności działania interpretera pojawia się istotna cecha odróżniająca go od kompilatora - jeśli interpreter wykonuje polecenia w miarę ich otrzymywania, to nie może przeprowadzić optymalizacji programu źródłowego. W rezultacie nie będzie fazy optymalizacji w ogólnej strukturze interpretera. W przeciwnym razie będzie niewiele różnił się od struktury podobnego kompilatora. Należy jedynie wziąć pod uwagę, że na ostatnim etapie – generowania kodu – polecenia maszynowe nie są zapisywane do pliku obiektowego, lecz są wykonywane w miarę ich generowania.

Brak kroku optymalizacji determinuje kolejną cechę charakterystyczną wielu interpreterów: bardzo często używają oni odwrotnej notacji polskiej jako wewnętrznej reprezentacji programu (patrz sekcja „Generowanie kodu. Metody generowania kodu”, rozdział 14). Ta wygodna forma reprezentacji operacji ma tylko jedną zasadniczą wadę - jest trudna do optymalizacji. Ale właśnie tego nie wymaga się od tłumaczy ustnych.

Nie wszystkie języki programowania pozwalają na budowę interpreterów, które mogłyby wykonywać program źródłowy po otrzymaniu poleceń.Aby to zrobić, język musi umożliwiać istnienie kompilatora, który analizuje program źródłowy w jednym przebiegu. Ponadto języka nie można interpretować w momencie odbierania poleceń, jeśli pozwala na pojawienie się wywołań funkcji i struktur danych przed ich bezpośrednim opisem. Dlatego języków takich jak C i Pascal nie można interpretować tą metodą.

Brak etapu optymalizacji prowadzi do tego, że wykonanie programu przy użyciu interpretera jest mniej wydajne niż przy użyciu podobnego kompilatora. Ponadto w przypadku interpretacji program źródłowy musi być analizowany od nowa przy każdym uruchomieniu, podczas gdy w przypadku kompilacji jest on analizowany tylko raz, a następnie zawsze używany jest plik obiektowy. Dlatego interpretery zawsze przegrywają z kompilatorami pod względem wydajności.

Zaletą interpretera jest to, że wykonanie programu jest niezależne od architektury docelowego systemu komputerowego. W wyniku kompilacji uzyskuje się kod obiektowy, który jest zawsze zorientowany na konkretną architekturę. Aby przeprowadzić migrację do innej architektury docelowego systemu komputerowego, program należy skompilować ponownie. Aby zinterpretować program, wystarczy mieć jego tekst źródłowy i tłumacz z odpowiedniego języka.

Przez długi czas tłumacze ustni byli znacznie rzadsi Który obieraczki. Z reguły istniały interpretery dla ograniczonego zakresu stosunkowo prostych języków programowania (takich jak Basic. Na bazie kompilatorów zbudowano wysokowydajne profesjonalne narzędzia do tworzenia oprogramowania.

Nowy impuls do rozwoju tłumaczy ustnych dał rozprzestrzenienie się języka globalnego sieć komputerowa. W takich sieciach mogą znajdować się komputery o architekturze osobistej, wtedy decydujący staje się wymóg jednolitego wykonania tekstu oryginalnego programu na każdym z nich. Dlatego wraz z rozwojem sieci globalnych i rozprzestrzenianiem się sieci WWW pojawił się Internet
We współczesnych systemach programowania istnieją implementacje oprogramowania, które łączą w sobie funkcje kompilatora i interpretera - w zależności od wymagań użytkownika program źródłowy jest albo kompilowany, albo wykonywany (interpretowany). Ponadto niektóre współczesne języki programowania obejmują dwa etapy rozwoju: najpierw program źródłowy jest kompilowany do kodu pośredniego (jakiś język niskiego poziomu), a następnie ten wynik kompilacji jest wykonywany przy użyciu interpretera tego języka pośredniego. Opcje dla takich systemów zostały omówione szerzej w rozdziale „Nowoczesne systemy programowania”.

Szeroko stosowanym przykładem języka interpretowanego jest HTML (Hypertext Markup Language), hipertekstowy język opisu. Na jego podstawie funkcjonuje obecnie niemal cała struktura Internetu. Inny przykład - Języki Java i JavaScript - łączą funkcje kompilacji i interpretacji. Tekst programu źródłowego jest kompilowany do postaci pośredniego kodu binarnego, niezależnego od architektury docelowego systemu komputerowego, który jest rozpowszechniany w sieci i wykonywany po stronie odbierającej – interpretowany.

Tłumacze z języka asemblera („asemblery”)

Język programowania - To jest język niskiego poziomu. Struktura i wzajemne powiązania łańcuchów tego języka są zbliżone do instrukcji maszynowych docelowego systemu komputerowego, w którym należy wykonać powstały program. Zastosowanie języka asemblera umożliwia programiście zarządzanie zasobami (procesorem, pamięcią RAM, urządzeniami zewnętrznymi itp.) docelowego systemu komputerowego z poziomu instrukcji maszynowych. W wyniku kompilacji każda instrukcja w źródłowym programie w języku asemblera jest konwertowana na jedną instrukcję maszynową.

Tłumaczem języka asemblera będzie oczywiście zawsze kompilator, gdyż językiem powstałego programu jest kod maszynowy. Tłumacz języka asemblera jest często nazywany po prostu „asemblerem” lub „programem asemblera”.

Implementacja kompilatorów z języka asemblera

Język asemblera zazwyczaj zawiera kody mnemoniczne dla instrukcji maszynowych. Najczęściej używana jest mnemonika poleceń w języku angielskim, ale istnieją inne warianty języków asemblera (w tym warianty w języku rosyjskim). Dlatego też asembler był kiedyś nazywany „mnemonicznym językiem kodowym” (obecnie tej nazwy praktycznie już nie używa się). Wszystkie możliwe polecenia w każdym języku asemblera można podzielić na dwie grupy: pierwsza grupa obejmuje polecenia języka zwykłego, które w procesie tłumaczenia są konwertowane na polecenia maszynowe; druga grupa składa się z poleceń języka specjalnego, które nie są konwertowane na polecenia maszynowe, ale służą kompilatorowi do wykonywania zadań kompilacji (takich jak na przykład zadanie alokacji pamięci). Składnia języka jest niezwykle prosta. Polecenia programu źródłowego są zwykle pisane w taki sposób, że w jednej linii programu znajduje się jedno polecenie. Każdą instrukcję języka asemblera można z reguły podzielić na trzy następujące po sobie elementy: etykiety, kod operacji i pole argumentu. Konwencjonalny kompilator języka asemblerowego przewiduje także możliwość umieszczania komentarzy w programie wejściowym oddzielonych od poleceń określonym ogranicznikiem.

Pole etykiety zawiera identyfikator reprezentujący etykietę lub jest puste. Każdy identyfikator etykiety może pojawić się tylko raz w programie w języku asemblera. Znak uważa się za opisany tam, gdzie się znajduje Na przeciętnie spotykane w programie (wymagany jest wstępny opis etykiet). Etykiety można użyć do przekazania kontroli wydanemu poleceniu. Często etykieta jest oddzielona od reszty polecenia cyklicznym separatorem (najczęściej dwukropkiem „:”).

Kod operacji jest zawsze ściśle określonym mnemonikiem jednego z możliwych poleceń procesora lub też ściśle określonym poleceniem (mojego kompilatora. Kod operacji zapisywany jest alfabetycznymi symbolami języka języka. Najczęściej jego długość wynosi 3-4, rzadziej - 5 lub 6 znaków.

Pole operandów jest albo puste, albo stanowi listę jednego, dwóch lub rzadziej trzech operandów. Liczba operandów jest ściśle określona i zależy od kodu operacji - każda operacja w języku asemblera udostępnia ściśle określoną liczbę swoich operandów. Odpowiednio każda z tych opcji odpowiada poleceniom bezadresowym, uniadresowym, dwuadresowym lub trzyadresowym (większa liczba operandów praktycznie nie jest używana; we współczesnych komputerach nawet polecenia trójadresowe są rzadkością). Jako opery; Mogą to być identyfikatory lub stałe.

Cechą języka asemblera jest to, że pewna liczba identyfikatorów w n jest przydzielana specjalnie do oznaczania rejestrów procesora. Fikatory takie z jednej strony nie wymagają wstępnego opisu, ale w przypadku D1 nie mogą być wykorzystane przez użytkownika do innych celów. Zestaw tych identyfikatorów jest predefiniowany dla każdego języka asemblera.

Czasami język asemblera pozwala na użycie jako operandów pewnych ograniczonych kombinacji oznaczeń rejestrów, identyfikatorów i stałych, które są łączone pewnymi znakami operatora. Takie zwroty są najczęściej używane do określenia typów adresowania, na przykład w instrukcjach maszynowych docelowego systemu komputerowego.

Na przykład następująca sekwencja poleceń

To jest przykład sekwencji poleceń języka asemblerowego; procesory z rodziny Intel 80x86. Jest polecenie opisu zbioru danych (db), etykieta (pętle), kody operacji (mov, dec i jnz). Operandy to identyfikator zbioru danych (data), oznaczenia rejestrów procesów

(bx i cx), etykieta (pętle) i stała (4). Złożone dane operandu odwzorowują pośrednie adresowanie zestawu danych do rejestru bazowego bx przy przesunięciu 4.

Taką składnię języka można łatwo opisać za pomocą gramatyki regularnej. Dlatego zbudowanie modułu rozpoznawania języka asemblera nie jest trudne. Z tego samego powodu w kompilatorach języka asemblera analizy leksykalne i składniowe są zwykle łączone w jeden moduł rozpoznawania.

Semantyka języka asemblera jest całkowicie zdeterminowana przez docelowy system obliczeniowy, dla którego ten język jest zorientowany. Semantyka języka asemblera określa, która instrukcja maszynowa odpowiada każdej instrukcji języka asemblera, a także które operandy i ich liczba są dozwolone dla danego kodu operacji.

Dlatego analiza semantyczna w kompilatorze języka asemblera jest tak prosta, jak analiza syntaktyczna. Jego głównym zadaniem jest sprawdzenie ważności operandów dla każdego kodu operacji, a także sprawdzenie, czy wszystkie identyfikatory i etykiety napotkane w programie wejściowym są opisane i oznaczające je identyfikatory nie odpowiadają predefiniowanym identyfikatorom używanym do oznaczania kodów operacji i rejestrów procesora.

Schematy analizy składniowej i analizy semantycznej w kompilatorze języka asemblerowego można zatem zaimplementować w oparciu o konwencjonalną maszynę skończoną. To właśnie ta cecha zadecydowała o tym, że kompilatory języka asemblera były w historii pierwszymi kompilatorami stworzonymi dla komputerów. Istnieje również szereg innych funkcji, które są specyficzne dla języków asemblerowych i upraszczają budowę ich kompilatorów.

Po pierwsze, kompilatory języka asemblerowego nie wymagają dodatkowej identyfikacji zmiennych - wszystkie zmienne językowe zachowują nazwy nadane im przez użytkownika. Za unikalność nazw w programie źródłowym odpowiada jego twórca, semantyka języka nie nakłada na ten proces żadnych dodatkowych wymagań. Po drugie, w kompilatorach języka asemblera alokacja pamięci jest niezwykle uproszczona. Kompilator języka asemblera działa tylko z pamięcią statyczną. Jeśli używana jest pamięć dynamiczna, do pracy z nią konieczne jest użycie odpowiedniej biblioteki lub funkcji systemu operacyjnego, a za jej przydział odpowiedzialny jest twórca programu źródłowego. Twórca programu źródłowego jest również odpowiedzialny za przekazywanie parametrów oraz organizację wyświetlania pamięci procedur i funkcji. Musi także zadbać o oddzielenie danych od kodu programu – kompilator języka asemblerowego, w przeciwieństwie do kompilatorów z języków wysokiego poziomu, nie dokonuje automatycznie takiego separacji. Po trzecie, na etapie generowania kodu w kompilatorze z języka asemblera nie przeprowadza się optymalizacji, ponieważ twórca programu źródłowego sam jest odpowiedzialny za organizację obliczeń, kolejność instrukcji maszynowych i dystrybucję rejestrów procesora.

Oprócz tych funkcji, kompilator języka asemblera jest zwykłym kompilatorem, ale znacznie uproszczonym w porównaniu do dowolnego kompilatora języka wysokiego poziomu. Kompilatory języka asemblera są najczęściej implementowane przy użyciu schematu dwuprzebiegowego. W pierwszym przebiegu kompilator analizuje program źródłowy, konwertuje go na kod maszynowy i jednocześnie wypełnia tablicę identyfikatorów. Ale przy pierwszym przebiegu instrukcji maszynowych adresy operandów znajdujących się w pamięci RAM pozostają puste. W drugim przebiegu kompilator wypełnia te adresy i jednocześnie wykrywa nieopisane identyfikatory. Dzieje się tak dlatego, że operand można zadeklarować w programie już po jego pierwszym użyciu. Wtedy jego adres nie jest jeszcze znany w momencie konstruowania instrukcji maszynowej i dlatego wymagany jest drugi przebieg. Typowym przykładem takiego operandu jest etykieta, która umożliwia przeskoczenie sekwencji instrukcji do przodu.

Makra i makra

Tworzenie programów w języku asemblera jest procesem dość pracochłonnym, często wymagającym prostego powtarzania tych samych operacji, które występują w kółko. Przykładem może być sekwencja poleceń wykonywanych za każdym razem w celu zorganizowania wyświetlania pamięci stosu podczas wchodzenia do procedury lub funkcji.

Aby ułatwić pracę programisty, stworzono tzw. makropolecenia.

Polecenie makro to podstawienie tekstu, podczas którego każdy identyfikator określonego typu zostaje zastąpiony ciągiem znaków z jakiegoś magazynu danych. Proces wykonania makropolecenia nazywa się makrogeneracją, a ciąg znaków powstały w wyniku wykonania makropolecenia nazywa się rozwinięciem makra.

Proces wykonywania makr polega na sekwencyjnym skanowaniu tekstu programu źródłowego, wykrywaniu w nim określonych identyfikatorów i zastępowaniu ich odpowiednimi ciągami znaków. Co więcej, dokonywana jest tekstowa zamiana jednego łańcucha znaków (identyfikatora) na inny ciąg znaków (string). Taka substytucja nazywana jest makrosubstytucją.

Definicje makr służą do określenia, które identyfikatory należy zastąpić jakimi ciągami znaków. Definicje makr znajdują się bezpośrednio w tekście programu źródłowego. Są one wyróżnione specjalnymi słowami kluczowymi lub ogranicznikami, które nie mogą pojawić się nigdzie indziej w tekście programu. Podczas przetwarzania wszystkie definicje makr są całkowicie wykluczane z tekstu programu wejściowego, a zawarte w nich informacje są zapisywane w celu przetworzenia podczas wykonywania makropoleceń.

Definicja makra może zawierać parametry. Następnie każde makropolecenie odpowiadające mu musi po wywołaniu zawierać ciąg znaków zamiast każdego parametru. Podczas wykonywania makra ciąg ten jest wstawiany w każdym miejscu, w którym w definicji makra pojawia się odpowiedni parametr. Parametrem makropolecenia może być inne makropolecenie, wówczas zostanie ono wywołane rekurencyjnie za każdym razem, gdy konieczne będzie podstawienie parametru. W zasadzie makroinstrukcje mogą tworzyć sekwencję

Wywołania rekurencyjne przypominają sekwencję wywołań rekurencyjnych procedur i funkcji, jednak zamiast obliczeń i przekazywania parametrów wykonują jedynie podstawienia tekstowe 1 .

Makra i definicje makr przetwarzane są przez specjalny moduł zwany makroprocesorem lub makrogeneratorem. Makrogenerator jako dane wejściowe otrzymuje tekst programu źródłowego, zawierający definicje makr i makropolecenia, a jego wyjście pojawia się jako tekst rozszerzenia makra programu źródłowego, niezawierający definicji makr i makropoleceń. Obydwa teksty są jedynie tekstami programu i nie są wykonywane żadne inne operacje. Jest to rozwinięcie makra tekstu źródłowego, które trafia na wejście kompilatora.

Składnia makropoleceń i definicji makr nie jest ściśle zdefiniowana. Może się różnić w zależności od implementacji kompilatora języka asemblera. Jednak sama zasada wykonywania podstawień makr w tekście programu pozostaje niezmieniona i nie zależy od ich składni.

Makrogenerator najczęściej nie istnieje jako oddzielny moduł oprogramowania, ale jest zawarty w kompilatorze języka asemblerowego. Rozszerzenie makra oryginalnego programu zwykle nie jest dostępne dla jego twórcy. Co więcej, podstawienia makr można wykonywać sekwencyjnie podczas analizowania tekstu źródłowego w pierwszym przebiegu kompilatora wraz z analizowaniem całego tekstu programu, a wtedy rozwinięcie makr programu źródłowego jako całości może w ogóle nie istnieć.

Na przykład następujący tekst definiuje makro push_0 w języku asemblera procesora Intel 8086:

Wieprz ah, ah ■ koniec topora pchającego

Semantyka tego makra polega na zapisaniu liczby „0” na stosie poprzez rejestr procesora ah. Następnie wszędzie w tekście programu, gdzie pojawia się instrukcja makra

Zostanie ono zastąpione w wyniku podstawienia makra ciągiem poleceń:

Wieprz ah, ah ■ pchnij topór

Jest to najprostsza wersja definicji makra. Możliwe jest tworzenie bardziej złożonych definicji makr z parametrami. Jedna z takich definicji makro została opisana poniżej:

Głębokość takiej rekurencji jest zwykle bardzo ograniczona. Sekwencja rekurencyjnych wywołań makropoleceń podlega zwykle znacznie surowszym ograniczeniom niż sekwencja rekurencyjnych wywołań procedur i funkcji, która przy stosowej organizacji wyświetlania pamięci jest ograniczona jedynie wielkością stosu przekazującego parametry. add_abx makro xl,x2

Pchnij topór
koniec

Wówczas makropolecenie również musi zostać wskazane w tekście programu odpowiednią liczbą parametrów. W tym przykładzie makro

Add_abx4,8 zostanie zastąpiony ciągiem poleceń w wyniku podstawienia makra:

Dodaj ah,4 dodaj bx.4 dodaj ex,8 pchnij topór

Wiele kompilatorów języka asemblera pozwala na jeszcze bardziej złożone konstrukcje, które mogą zawierać zmienne lokalne i etykiety. Przykładem takiej konstrukcji jest makrodefinicja:

Loop_ax makro xl,x2,yl

Hog bx.bx pętla: dodaj bx.yl

Tutaj etykieta 1 oopax jest lokalna i zdefiniowana tylko w ramach tej definicji makra. W takim przypadku nie można już wykonać prostego podstawienia makropolecenia do tekstu programu, ponieważ dwukrotne wykonanie tego makropolecenia spowoduje pojawienie się dwóch identycznych etykiet 1 oorax w tekście programu. W tym wykonaniu generator makr musi zastosować bardziej złożone techniki podstawienia tekstu, podobne do tych stosowanych przez kompilatory do identyfikacji elementów leksykalnych programu wejściowego, aby nadać wszystkim możliwym zmiennym lokalnym i etykietom makr unikalne nazwy w całym programie. Makra i makroinstrukcje znalazły zastosowanie nie tylko w językach asemblera, ale także w wielu językach wysokiego poziomu. Tam są one przetwarzane przez specjalny moduł zwany preprocesorem języka (powszechnie znany jest np. preprocesor języka C). Zasada przetwarzania pozostaje taka sama jak w przypadku programów w języku asemblera – preprocesor dokonuje podstawień tekstowych bezpośrednio w wierszach samego programu źródłowego. W językach wysokiego poziomu definicje makr muszą być oddzielone od tekstu samego programu źródłowego, aby preprocesor nie mógł ich pomylić z konstrukcjami syntaktycznymi języka wejściowego. Aby to zrobić, stosuje się albo specjalne symbole i polecenia (polecenia preprocesora), które nigdy nie mogą pojawić się w tekście programu źródłowego, albo występują definicje makr

Wewnątrz nieznacznej części programu źródłowego - są one zawarte w komentarzach (taka implementacja istnieje na przykład w kompilatorze Pascala stworzonym przez firmę Borland). Natomiast makropolecenia mogą pojawić się w dowolnym miejscu tekstu źródłowego programu, a ich składniowe wywołanie nie może różnić się od wywoływania funkcji w języku wejściowym.

Należy pamiętać, że pomimo podobieństwa składni wywołań makropolecenia zasadniczo różnią się od procedur i funkcji, gdyż nie generują kodu wynikowego, a jedynie podstawienia tekstu wykonywane bezpośrednio w tekście programu źródłowego. Wynik wywołania funkcji i makra może z tego powodu znacznie się różnić.

Spójrzmy na przykład w C. Jeśli opisano funkcję

Int fKint a) ( return a + a: ) i podobne makropolecenie

#define f2(a) ((a) + (a)) wtedy wynik ich wywołania nie zawsze będzie taki sam.

Rzeczywiście, wywołania j=fl(i) i j=f2(i) (gdzie i i j to pewne zmienne całkowite) doprowadzą do tego samego wyniku. Ale wywołania j=fl(++i) i j=f2(++i) dadzą różne znaczenia zmienna j. Faktem jest, że skoro f2 jest definicją makra, to w drugim przypadku dokonana zostanie podstawienie tekstu, w wyniku którego otrzymamy ciąg operatorów j=((++i) + (++i)). Widać, że w tej sekwencji operacja ++i zostanie wykonana dwukrotnie, w przeciwieństwie do wywołania funkcji fl(++i), gdzie jest ona wykonywana tylko raz.

Ponieważ tekst programu napisanego w języku programowania nie jest zrozumiały dla komputera, należy go przetłumaczyć na język maszynowy. Tłumaczenie programu z języka programowania na język kodu maszynowego nazywa się audycja(tłumaczenie - tłumaczenie) i jest to wykonywane przez specjalne programy - nadawcy.

Istnieją dwa rodzaje tłumaczy: interpretery i kompilatory.

Interpretator nazywany jest tłumaczem, który wykonuje tłumaczenie instrukcja po instrukcji (instrukcja po poleceniu), a następnie wykonanie przetłumaczonej instrukcji programu źródłowego. Dwie wady metody interpretacji:

1. program do tłumaczenia ustnego musi znajdować się w pamięci komputera przez cały czas wykonywania programu oryginalnego, czyli zajmować określoną ilość pamięci;

2. proces tłumaczenia tego samego operatora powtarza się tyle razy, ile polecenie to musi zostać wykonane w programie.

Kompilator to program, który konwertuje (tłumaczy) program źródłowy na program (moduł) w języku maszynowym. Następnie program jest zapisywany w pamięci komputera i dopiero wtedy wykonywany.

Podczas kompilacji procesy tłumaczenia i wykonania są rozdzielone w czasie: najpierw program źródłowy jest całkowicie tłumaczony na język maszynowy (po czym nie jest wymagana obecność tłumacza w pamięci RAM), a następnie przetłumaczony program można wykonać wielokrotnie .

Każdy tłumacz rozwiązuje następujące główne zadania:

1. Analizuje przetłumaczony program i stwierdza, czy zawiera on błędy składniowe;

2. Generuje program wyjściowy w języku poleceń komputera;

3. Przydziela pamięć dla programu wyjściowego, tj. Każdej zmiennej, stałej, tablicom i innym obiektom przydzielana jest osobna sekcja pamięci.

Zatem, Kompilator(Język angielski) kompilator- kompilator, kolektor) czyta cały program całkowicie, tłumaczy go i tworzy kompletną wersję programu w języku maszynowym, która następnie jest wykonywana.

Interpretator(Język angielski) interpretator- interpreter, interpreter) tłumaczy i wykonuje program linia po linii.

Po skompilowaniu programu nie jest już potrzebny ani program źródłowy, ani kompilator. Jednocześnie program przetwarzany przez interpreter musi zostać ponownie przetworzony przenosić na język maszynowy przy każdym uruchomieniu programu.

Każdy konkretny język jest nastawiony albo na kompilację, albo na interpretację – w zależności od celu, dla którego został stworzony. Na przykład, Pascal zwykle używany do rozwiązywania raczej złożonych problemów, w których ważna jest szybkość programu. Dlatego język ten jest zwykle implementowany przy użyciu kompilator. Z drugiej strony, PODSTAWOWY powstał jako język dla początkujących programistów, dla których wykonywanie programu linia po linii ma niezaprzeczalne zalety. Czasami dla jednego języka istnieje i kompilator, i tłumacz. W takim przypadku możesz użyć interpretera do opracowania i przetestowania programu, a następnie skompilować debugowany program, aby poprawić jego szybkość wykonywania.

Ponieważ tekst napisany w języku programowania jest niezrozumiały dla komputera, należy go przetłumaczyć na kod maszynowy. To tłumaczenie programu z języka programowania na język kodu maszynowego nazywa się tłumaczeniem i wykonują je specjalne programy - tłumacze.

Tłumacz to program usługowy, który konwertuje program źródłowy dostarczony w wejściowym języku programowania na działający program prezentowany w języku obiektowym.

Obecnie tłumaczy dzieli się na trzy główne grupy: asemblery, kompilatory i interpretery.

Asembler to systemowy program narzędziowy, który konwertuje struktury symboliczne na polecenia języka maszynowego. Specyficzną cechą asemblerów jest to, że dokonują one dosłownego tłumaczenia jednej instrukcji symbolicznej na jedną instrukcję maszynową. Zatem język asemblera (zwany także autokodem) ma na celu ułatwienie percepcji systemu poleceń komputera i przyspieszenie programowania w tym systemie poleceń. Programiście znacznie łatwiej jest zapamiętać mnemoniczne oznaczenie instrukcji maszynowych niż ich kod binarny.

Jednocześnie język asemblera, oprócz analogów poleceń maszynowych, zawiera wiele dodatkowych dyrektyw ułatwiających w szczególności zarządzanie zasobami komputera, pisanie powtarzających się fragmentów i budowanie programów wielomodułowych. Dlatego ekspresja języka jest znacznie bogatsza niż tylko symboliczny język kodowania, co znacznie poprawia efektywność programowania.

Kompilator to program usługowy, który tłumaczy program napisany w źródłowym języku programowania na język maszynowy. Podobnie jak asembler, kompilator konwertuje program z jednego języka na inny (najczęściej na język konkretnego komputera). Jednocześnie polecenia w języku źródłowym różnią się znacznie pod względem organizacji i mocy od poleceń w języku maszynowym. Istnieją języki, w których jedno polecenie języka źródłowego jest tłumaczone na 7-10 poleceń maszynowych. Istnieją jednak również języki, w których każde polecenie może mieć 100 lub więcej poleceń maszynowych (na przykład Prolog). Ponadto języki źródłowe dość często stosują ścisłe typowanie danych, przeprowadzane poprzez ich wstępny opis. Programowanie może polegać nie na kodowaniu algorytmu, ale na dokładnym przemyśleniu struktur danych lub klas. Proces tłumaczenia z takich języków nazywany jest zwykle kompilacją, a języki źródłowe zazwyczaj klasyfikowane są jako języki programowania wysokiego poziomu (lub języki wysokiego poziomu). Abstrakcja języka programowania z komputerowego systemu poleceń doprowadziła do niezależnego stworzenia szerokiej gamy języków skupionych na rozwiązywaniu konkretnych problemów. Pojawiły się języki do obliczeń naukowych, obliczeń ekonomicznych, dostępu do baz danych i innych.

Tłumacz - program lub urządzenie, które dokonuje tłumaczenia i wykonywania programu źródłowego przez operatora. W przeciwieństwie do kompilatora, interpreter nie generuje programu w języku maszynowym jako wyniku. Po rozpoznaniu polecenia w języku źródłowym natychmiast je wykonuje. Zarówno kompilatory, jak i interpretery używają tych samych metod analizy kodu źródłowego programu. Ale interpreter pozwala rozpocząć przetwarzanie danych po napisaniu choćby jednego polecenia. Dzięki temu proces tworzenia i debugowania programów jest bardziej elastyczny. Ponadto brak wyjściowego kodu maszynowego pozwala nie „zaśmiecać” urządzeń zewnętrznych dodatkowymi plikami, a sam interpreter można dość łatwo dostosować do dowolnej architektury maszyny, opracowując go tylko raz w powszechnie używanym języku programowania. Dlatego powszechne stały się języki interpretowane, takie jak Java Script i VB Script. Wadą interpreterów jest niska prędkość wykonywania programu. Zazwyczaj interpretowane programy działają od 50 do 100 razy wolniej niż programy natywne.

Emulator to program lub narzędzie programowo-sprzętowe, które umożliwia, bez przeprogramowania, wykonanie na danym komputerze programu, który wykorzystuje kody lub metody wykonywania operacji inne niż na danym komputerze. Emulator jest podobny do interpretera w tym sensie, że bezpośrednio wykonuje program napisany w określonym języku. Najczęściej jest to jednak język maszynowy lub kod pośredni. Obydwa reprezentują instrukcje w kodzie binarnym, które można wykonać natychmiast po rozpoznaniu kodu operacji. W przeciwieństwie do programów tekstowych nie ma potrzeby rozpoznawania struktury programu ani wybierania argumentów.

Emulatory są używane dość często do różnych celów. Na przykład podczas opracowywania nowych systemów komputerowych najpierw tworzony jest emulator, który uruchamia programy opracowane dla komputerów, które jeszcze nie istnieją. Pozwala to na ocenę systemu dowodzenia i opracowanie podstawowego oprogramowania jeszcze przed utworzeniem odpowiedniego sprzętu.

Bardzo często do uruchamiania starych programów na nowych komputerach używany jest emulator. Zazwyczaj nowsze komputery są szybsze i mają lepsze urządzenia peryferyjne. Pozwala to na skuteczniejszą emulację starszych programów niż uruchamianie ich na starszych komputerach.

Transkoder to program lub urządzenie programowe, które tłumaczy programy napisane w języku maszynowym jednego komputera na programy w języku maszynowym innego komputera. Jeśli emulator jest mniej inteligentnym odpowiednikiem interpretera, wówczas transkoder działa w tej samej roli w stosunku do kompilatora. Podobnie źródłowy (zwykle binarny) kod maszynowy lub reprezentacja pośrednia są konwertowane na inny podobny kod za pomocą jednej instrukcji i bez ogólnej analizy struktury programu źródłowego. Transkodery są przydatne podczas przenoszenia programów z jednej architektury komputera na inną. Można ich również używać do rekonstrukcji tekstu programu w języku wysokiego poziomu z istniejącego kodu binarnego.

Makroprocesor to program, który zastępuje jeden ciąg znaków innym. Jest to rodzaj kompilatora. Generuje tekst wyjściowy poprzez przetwarzanie specjalnych wstawek znajdujących się w tekście źródłowym. Wkładki te są zaprojektowane w specjalny sposób i należą do konstrukcji języka zwanego makrojęzykiem. Makroprocesory są często stosowane jako dodatki do języków programowania, zwiększające funkcjonalność systemów programistycznych. Prawie każdy asembler zawiera makroprocesor, który zwiększa efektywność tworzenia programów maszynowych. Takie systemy programowania nazywane są zwykle makroasemblerami.

Makroprocesory są również używane w językach wysokiego poziomu. Zwiększają funkcjonalność języków takich jak PL/1, C, C++. Makroprocesory są szczególnie szeroko stosowane w językach C i C++, co ułatwia pisanie programów. Makroprocesory poprawiają efektywność programowania bez zmiany składni i semantyki języka.

Składnia to zbiór reguł języka, które określają powstawanie jego elementów. Innymi słowy, jest to zbiór reguł tworzenia semantycznie znaczących ciągów symboli w danym języku. Składnię określa się za pomocą reguł opisujących pojęcia języka. Przykładowe pojęcia to: zmienna, wyrażenie, operator, procedura. Kolejność pojęć i ich dopuszczalne użycie w regułach determinuje poprawne składniowo struktury tworzące programy. To hierarchia obiektów, a nie sposób, w jaki współdziałają ze sobą, jest definiowana za pomocą składni. Na przykład instrukcja może wystąpić tylko w procedurze, wyrażenie w instrukcji, zmienna może składać się z nazwy i opcjonalnych indeksów itp. Składnia nie jest kojarzona z takimi zjawiskami w programie jak „przeskakiwanie do nieistniejącej etykiety” czy „nie zdefiniowana zmienna o podanej nazwie”. To właśnie robi semantyka.

Semantyka - reguły i warunki określające relacje pomiędzy elementami języka i ich znaczeniami semantycznymi, a także interpretacja znaczenia znaczeniowego konstrukcji syntaktycznych języka. Obiekty języka programowania nie tylko są umieszczone w tekście według określonej hierarchii, ale dodatkowo są ze sobą powiązane poprzez inne pojęcia, tworzące rozmaite skojarzenia. Na przykład zmienna, dla której składnia określa prawidłowe położenie tylko w deklaracjach i niektórych instrukcjach, ma określony typ, może być używana z ograniczoną liczbą operacji, ma adres, rozmiar i musi zostać zadeklarowana, zanim będzie mogła być użyte w programie.

Parser to komponent kompilatora, który sprawdza instrukcje źródłowe pod kątem zgodności z regułami składniowymi i semantyką danego języka programowania. Pomimo swojej nazwy analizator sprawdza zarówno składnię, jak i semantykę. Składa się z kilku bloków, z których każdy rozwiązuje własne problemy. Zostanie to omówione bardziej szczegółowo przy opisywaniu struktury tłumacza.

Każdy tłumacz wykonuje następujące główne zadania:

Analizuje przetłumaczony program, w szczególności stwierdza, czy zawiera on błędy składniowe;

Generuje program wyjściowy (często nazywany programem obiektowym) w języku instrukcji maszynowych;

Przydziela pamięć dla programu obiektowego.