Pokazuję tu kompletny prosty kompilator napisany w czystym C (bez użycia dodatkowych narzędzi). Kompilator tłumaczy prosty język na kod w C (który można następnie przekompilować kompilatorem C i uzyskać program wykonywalny). Język jest minimalny: udostępnia tylko instrukcje podstawienia i wypisywania wyników. Zmienne mają nazwy jednoliterowe, przyjmują wartości całkowite i nie trzeba ich deklarować.`
Analizator leksykalny jest ad hoc. Symbolami są słowo 'pisz', liczby, litery (jako nazwy zmiennych), operatory arytmetyczne, średnik i znak równości. Większość symboli jest jednoznakowa, tylko liczby i słowo 'pisz' są wieloznakowe. Za wyjątkiem słowa 'pisz' pierwszy znak pozwala rozstrzygnąć jaki symbol widzimy. W przypadu litery 'p' nastepny znak decyduje, jesli jest to 'i' to jedyna możliwość to słowo 'pisz', inne znaki oznaczają że 'p' to nazwa zmiennej.
Aby móc używać symboliczne nazwy dla różnych rodzajów symboli deklarujemy typ wyliczny 'rodzaj_symbolu'. Oprócz symboli stanowiących część programy mam trzy specjalne symbole: 'sym_eof' sygnalizuje koniec programu, 'sym_blad' oznacza błędny znak, 'sym_start' jest używany przez analizator syntaktyczny. Analizator leksykalny odczytyje z tablicy 'tabela_rodzaju' rodzaj aktualnie widzianego symbolu. Litera 'p' wymaga sprawdzenia (i jeśli trzeba poprawki). Dla liczb w pętli obliczamy ich wartość.
Rozpoznanie liczb i zmiennaj 'p' wymaga oglądania następnego znaku, dlatego analizator zapamietuje jeden znak "podglądu" w zmiennej 'biezacy_znak'. Normalnie zawiera ona pierszy znak za aktualnie rozpoznanym symbolem.
Analizator syntaktyczny dla wyrażen bazuje na priorytetach operatorów. Jest to wariant metody z zadania 4 listy 3 na ćwiczenia. Tyle że zamiast budawać jawnie table decyzyjne kodujemy decyzje przy pomocy instrukcji 'if' i 'while'. Robimy też parę uproszczeń. Dla wyrażeń istotna cześć stanu to priorytet operatora, i tylko to pamiętamy. Ponieważ priorytet możemy wyliczyć z operatora na stosie, nie ma potrzeby zapamiętywania na stosie stanów, wystarczą same symbole. Ponieważ po tych uproszczeniach straciliśmy informację o tym czy podwyrażenie na stosie jest kompletne musimy przy redukcji sprawdzać poprawność (i tak robimy).
Zmienne i operatory zapamiętujemy na stosie tak długo aż zobaczymy operator o niższym (lub równym) priorytecie (lub nawias zamykający czy średnik). Po rozpoznaniu elementarnego podwyrażenia (tzn. trójki argument1 operator argument2) zastępujemy je na stosie pojedynczym węzłem drzewa rozbioru (który zachowuje się dalej podobnie jak zmienne). Specjana instrukcja 'if' rozpoznaje wyrażenia w nawiasach. W momencie gdy ta instrukcja jest wykonywana wyrażenie wewnątrz nawiasów jest już zastąpione pojedynczym węzłem drzewa.
Powną wadą analizatora (spowodowaną naszymi uproszczeniami) jest opóźnione wykrywanie błędów. Mianowicie, dla wyrażenia takiego jak 'xyz;' dopiero po zobaczeniu średnika analizator próbuje rozpoznać podwyrażenia i zauważa błąd.
Obsługa błędów jest minimalna: wypisujemy komunikat o błędzie i kończymy pracę. Zakładamy że 'malloc' się uda i nie próbujemy zwalniać pamięci (większość jest potrzebna aż do końca).