Program rysujący schemat układu logicznego opisanego w zaprojektowanym przez siebie języku

Program ten został napisany przeze mnie na VI semestrze studiów na laboratorium z przedmiotu o nazwie "Języki formalne i kompilatory". Do realizacji projektu wykorzystałem język Java i środowisko NetBeans.

    Tutaj znajduje się plik jar z programem (prezentowana tutaj wersja różni się od akademickiej tym, że w tamtej wersji kod był wczytywany z pliku, zaś w tej wersji, aby ułatwić obsługę, kod jest wprowadzane przez powstałe podczas tworzenia tej strony gui).

  1. Przedstawienie zadania

  2. Należało napisać program rysujący schemat układu logicznego opisanego w zaprojektowanym przez siebie języku. Język miał obsługiwać zdefiniowane elementy takie jak bramki OR, AND, NOT, XOR, wejście-wyjście oraz połączenia pomiędzy nimi. Język powinien umożliwiać tworzenie tzw. węzłów, czyli własnych komponentów składających się z wielu elementów (przykładem może być multiplekser).

    Aplikacja powinna zostać napisana w języku Java, mogła być wyposażona w GUI do prezentacji wynikowego schematu. Analiza leksykalna i składniowa zrobiona powinna być zrobiona przy pomocy własnych klas o strukturze zgodnej ze strukturą kompilatorów LL(1) przedstawioną na wykładzie. Aplikacje powinny być zaprojektowane przy wykorzystaniu diagramów w języku UML oraz zaimplementowane przy wykorzystaniu projektowania obiektowego.

  3. Opis języka

  4. Ogólne założenie języka jest takie, że pierwszy leksem w wierszu określa jakiego rodzaju element definiujemy (bramka, wezel, wejście, wyjście). Drugim elementem jest określenie identyfikatora urządzenia (nie mogą się powtarzać i muszą być mniejsze od 40), trzeci element określa, ile będzie wejść do urządzenia. Kolejne elementy to numery wejść do urządzenia. Należy je zakończyć średnikiem, co oznacza koniec wejść. W przypadku różnicy pomiędzy liczbą zadeklarowanych wejść a rzeczywistą ich liczbą program poinformuje nas o błędzie i kod nie będzie skompilowany. W przypadku definiowania węzłów obowiązują trochę inne zasady.
    Przykłady użycia (2-wejściowa bramka or):

    wejscie u1
    wejscie u2
    or u3 we2 1 2 ;
    wyjscie u4 3

    W przypadku tworzenia węzłów obowiązują następujące reguły:
    - tworzenie węzła: ostatni element węzła jest jego wyjściem, nazwy urządzeń użyte w produkcji węzła będą zmienione
    - wejścia w poleceniu wezel to te , które podczas tworzenia w tworzwezel zaczynały się od litery z
    - numerowanie elementów w węzłach zaczyna się od numeru 40 (kolejność występowania w kodzie) więc istnieje możliwość podłączenia wyjść lub kolejnych bramek pod elementy zawarte w węzłach

    wejscie u1
    wejscie u2
    tworzwezel_{
    or u1 we2 z1 z2 ;
    and u2 we3 1 z1 z2 ;
    } u3
    wezel u3 we2 1 2
    wyjscie u4 42

  5. Definicja gramatyki bezkontekstowej

  6. Ʃ = {'T_OR', 'T_AND', 'T_NOT', 'T_XOR', 'T_NUMERURZADZENIA', 'T_LICZBAWEJSC', 'T_NUMERWEJSCIA', 'T_KONIECPROGRAMU', 'T_WEZEL', 'T_KONIECWEZLA', 'T_WEJSCIE', 'T_WYJSCIE', 'T_KONIECWEJSC', 'T_NUMERWEJSCIAZEWNETRZNEGO', 'T_TWORZWEZEL'}

    Γ = {'URZADZENIE', 'NAZWAURZADZENIA', 'WEJSCIA', 'WEZEL', 'NOWYWEZEL', 'CIAŁO'}

    S = 'CIALO'

    P = {
    CIALO → URZADZENIE CIALO
    CIALO → NOWYWEZEL CIALO
    CIALO → T_KONIECPROGRAMU
    URZADZENIE → NAZWAURZADZENIA T_NUMERURZADZENIA
    T_LICZBAWEJSC WEJSCIA
    URZADZENIE → T_WEJSCIE T_NUMERURZADZENIA
    URZADZENIE → T_WYJSCIE T_NUMERURZADZENIA T_NUMERWEJSCIA
    NAZWAURZADZENIA → T_OR
    NAZWAURZADZENIA → T_AND
    NAZWAURZADZENIA → T_NOT
    NAZWAURZADZENIA → T_XOR
    NAZWAURZADZENIA → T_WEZEL
    NOWYWEZEL → T_TWORZWEZEL WEZEL
    WEZEL → URZADZENIE WEZEL
    WEZEL → T_KONIECWEZLA T_NUMERURZADZENIA
    WEJSCIA → T_NUMERWEJSCIA WEJSCIA
    WEJSCIA → T_NUMERWEJSCIAZEWNETRZNEGO WEJSCIA
    WEJSCIA → T_KONIECWEJSC
    }


    lookahead1(CIALO) = {'T_OR'}
    lookahead1(CIALO) = {'T_AND'}
    lookahead1(CIALO) = {'T_NOT'}
    lookahead1(CIALO) = {'T_XOR'}
    lookahead1(CIALO) = {'T_WEZEL'}
    lookahead1(CIALO) = {'T_WEJSCIE'}
    lookahead1(CIALO) = {'T_WYJSCIE'}
    lookahead1(CIALO) = {'T_TWORZWEZEL'}
    lookahead1(CIALO) = {'T_KONIECPROGRAMU'}
    zbiory rozłączne
    lookahead1(URZADZENIE) = {'T_OR'}
    lookahead1(URZADZENIE) = {'T_AND'}
    lookahead1(URZADZENIE) = {'T_NOT'}
    lookahead1(URZADZENIE) = {'T_XOR'}
    lookahead1(URZADZENIE) = {'T_WEZEL'}
    lookahead1(URZADZENIE) = {'T_WEJSCIE'}
    lookahead1(URZADZENIE) = {'T_WYJSCIE'}
    zbiory rozłączne
    lookahead1(NAZWAURZADZENIA) = {'T_OR'}
    lookahead1(NAZWAURZADZENIA) = {'T_AND'}
    lookahead1(NAZWAURZADZENIA) = {'T_NOT'}
    lookahead1(NAZWAURZADZENIA) = {'T_XOR'}
    lookahead1(NAZWAURZADZENIA) = {'T_WEZEL'}
    zbiory rozłączne
    lookahead1(WEZEL) = {'T_OR'}
    lookahead1(WEZEL) = {'T_AND'}
    lookahead1(WEZEL) = {'T_NOT'}
    lookahead1(WEZEL) = {'T_XOR'}
    lookahead1(WEZEL) = {'T_WEZEL'}
    lookahead1(WEZEL) = {'T_TWORZWEZEL'}
    lookahead1(WEZEL) = {'T_KON'}
    zbiory rozłączne
    lookahead1(WEJSCIA) = {'T_NUMERWEJSCIA'}
    lookahead1(WEJSCIA) = {'T_KONIECWEJSC'}
    lookahead1(WEJSCIA) = {'T_NUMERWEJSCIAZEWNETRZNEGO'}
    zbiory rozłączne
    1. Wyrażenia regularne opisujące język

    2. T_OR or
      T_AND and
      T_NOT not
      T_NUMERURZADZENIA u[0-9]{1,3}
      T_LICZBAWEJSC we[0-9]{1,3}
      T_NUMERWEJSCIA [0-9]{1,3}
      T_WEZEL wezel
      T_KONIECWEZLA \\}
      T_WEJSCIE wejscie
      T_WYJSCIE wyjscie
      T_KONIECWEJSC ;
      T_NUMERWEJSCIAZEWNETRZNEGO Z[0-9]{1,3}
      T_TWORZWEZEL tworzwezel_\\{
      T_KONIECPROGRAMU EOF
  7. Opis najistotniejszych fragmentów kodu źródłowego

  8. Klasa Rozruch odpowiedzialna jest za uruchamianie kolejnych komponentów programu. Używamy flagi poprawny_kod aby wiedzieć, czy poprzednie etapy sprawdzania kodu dały pozytywny rezultat. Jeśli nie, dalsze działania nie są wykonywane. Pierwszą czynnością jest wczytanie do tablicy kodu wysłanego przez użytkownika. Następnie uruchamiamy analizator leksykalny. W przypadku poprawnych leksemów przechodzimy do analizatora składniowego. Gdy ten także nie zmieni flagi na 0, tworzymy drzewo składni i tworzymy elementy z węzłów. Wszystkie komponenty zapisujemy do nowej tablicy. Następnie ustalamy, który element w jakim bloku zostanie wyświetlony, drukujemy drzewo składni i inicjujemy grafikę.

    Funkcja Lookahead w analizatorze składniowym sprawdza, czy dany leksem ma prawo znajdować się w określonym miejscu kodu. Iteracyjnie sprawdzamy wszystkie leksemy, które mogą wystąpić dla danego symbolu nieterminalnego. Dzięki zastosowaniu gramatyki LL(1) wystarczy sprawdzenie tylko jednego leksemu. W przypadku uznania, że dany leksem jest w odpowiednim miejscu, zwracamy liczbę odpowiadającą numerowi produkcji, która została tutaj wykonana. W innym przypadku funkcja zwróci wartość -1, co zostanie zinterpretowane jako błąd i spowoduje wyświetlenie odpowiedniego błędu.

  9. Zrzuty ekranu:


Widok startowy Po udanej kompilacji kompilator zwraca drzewo składni (w dolnym textarea) Prezentacja przykładowego układu logicznego z dwoma węzłami Przykład błędu znalezionego przez analizator leksykalny (niepoprawny leksem) Przykład błędu znalezionego przez analizator składniowy (w linijce: or and u9 we2 4 5 ; - podwójna deklaracja rodzaju bramki)




Projekt i wykonanie: Krzysztof Ruszczyński (w oparciu o własny system CMS)
Grafika dostarczona przez jcd.pl