ktoś@gdzieś ~ $make coś
Pewnie zazwyczaj w ten właśnie sposób kompilujesz swoje programy. Czasem po tej instrukcji pokazują się błędy składniowe jakie popełniłeś pisząc swój program. Na przykład jak zgubi się średnik na końcu instrukcji. A wiesz jak kompilator odnajduje te błędy? A jak napiszesz printf to skąd wie czy traktorać go jako funkcje czy jako zmienną? Spróbuję choć trochę rozjaśnić działanie kompilatora.
Na początek spróbujmy zdefiniować funkcję warunku.
<instrukcja> -> if<warunek>
<instrukcja>
else
<instrukcja>
<instrukcja> może oznaczać dowolną funkcję, pętlę, przypisywanie danych itp. Zatem podobnie możemy zdefiniować pętlę for.
<instrukcja> -> for(<instrukcja>;<warunek>;<instrukcja>)
<instrukcja>
<warunek> też może przybierać różne formy.
<warunek> -> <wyrażenie> == <wyrażenie>
<warunek> -> <wyrażenie> > <wyrażenie>
<warunek> -> <wyrażenie> < <wyrażenie>
<warunek> -> <wyrażenie> >= <wyrażenie>
<warunek> -> <wyrażenie> <= <wyrażenie>
A wyrażenie to:
<wyrażenie> -> <liczba>
<wyrażenie> -> <zmienna>
<wyrażenie> -> <wyrażenie> + <wyrażenie>
Nie podałam oczywiście wszystkich możliwych interpretacji instrukcji, warunku i wyrażenia. Są to tylko przykłady. Jest to podobne do gramatyki, którą posługują się kompilatory sprawdzając poprawność kodu.
Notacja BNF
Warto w tym momencie wspomnieć o notacji Bachusa-Naura (BNF - Bachus-Naur-Form). Jest to język służący do opisu składni języków formalnych. Wygląda trochę podobnie do definicji, które napisłam wyżej, ale na wszelki wypadek podam kilka przykładów składni Pascala zapisanych w tej notacji.
<litera>::=A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|
a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z
<cyfra>::=0|1|2|3|4|5|6|7|8|9
<ciag_cyfr>::=<cyfra>{<cyfra>}
Gdzie:
::= oznacza równe z definicji (wyżej zapisywałam to jako'->')
| to alternatywa
{} to dowolny ciąg powtórzeń
Wróćmy znowu do kompilacji. Składa się ona z czterech faz:
I analizator leksykalny (scanner)
Grupuje słowa i odróżnia operatory. Mozna powiedzieć, że łączy literki jak dzieci w pierwszej klasie. Jest to istotne by kolejne fazy mogły łatwiej rozpoznawać funkcje i zmienne. Na przykład inaczej pogrupuje funkcję printf() i zmienną printf.
II analiaztor składni (parser)
Analizuje dane wejściowe w celu określenia ich gramatycznej struktury, można powiedzieć, że czyta kod źródłowy ze zrozumieniem. Zorientuje się czy printf jest funkcją czy zmienną.
III analizator sematyczny
Dopiero tu sprawdzana jest poprawność programu. Przegląda instrukcje i wytyka wszystkie błędy gramatyczne. Przypomni, że w printf("coś"; zapomnieliśmy dać ')' przed ';'.
IV generacja kodu
W ostatniej fazie kod źródłowy przekształcany jest na kod zrozumiały dla maszyny. Teraz nasz nowo powstały program można otworzyć i zadziała.