Skip to content

Commit

Permalink
Etap 5
Browse files Browse the repository at this point in the history
  • Loading branch information
Dawid Sygocki committed Jun 8, 2022
1 parent e436c39 commit 0d823e5
Show file tree
Hide file tree
Showing 88 changed files with 2,771 additions and 1,076 deletions.
60 changes: 38 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,22 @@ Przeważająca większość konstruktów językowych to wyrażenia, co ma uczyni
Obejmuje to m.in.:
* bloki kodu, które zwracają wartość ostatniego wyrażenia w nich zawartego (lub `null`, jeśli są puste),
* pętle `for` i `while` zwracające po zakończeniu wartość odpowiednio licznika oraz sprawdzanego warunku,
* przypisania wartości do zmiennych.
* przypisania wartości do zmiennych (nie chodzi tu o deklaracje).

Język implementuje funkcje anonimowe i pozwala na przypisywanie ich do zmiennych.

Podstawowe typy danych będą przekazywane do funkcji przez kopię, natomiast łańcuchy znaków (których zawartość jest niezmienna) poprzez referencję.
Argumenty są przekazywane do funkcji przez kopię.


## Formalna specyfikacja i składnia

Gramatyka realizowanego języka opisana jest w pliku [gramatyka.md](docs/gramatyka.md). Reguły dotyczące operatorów są zgodne z tabelami z pliku [operatory.md](docs/operatory.md).

Nie przewiduje się na razie konfiguracji zachowania interpretera poprzez specjalne pliki.
Nie przewiduje się konfiguracji zachowania interpretera poprzez specjalne pliki.

Możliwe jest importowanie zawartości innych skryptów za pomocą instrukcji `pull`. Rolę biblioteki standardowej pełni przestrzeń nazw `std`. Zdefiniowane w "dociąganym" skrypcie elementy są wprowadzanie do przestrzeni nazw skryptu głównego, a w wypadku konfliktu nazw można odwołać się do nich z użyciem pełnej ścieżki (np. `std.io.print`).
Planowane było umożliwienie użytkownikowi importowania zawartości innych skryptów za pomocą instrukcji `pull`. Rolę biblioteki standardowej miała pełnić przestrzeń nazw `std`. Zdefiniowane w "dociąganym" skrypcie elementy miały być wprowadzane do przestrzeni nazw skryptu głównego, a w wypadku konfliktu nazw możliwe miało być odwołanie się do nich z użyciem pełnej ścieżki (np. `std.io.print`).

Ze względu na obecny brak wsparcia (w klasie interpretera) dla instrukcji `pull`, funkcje wbudowane `print` oraz `quit` zlokalizowane są bezpośrednio w środowisku użytkownika.

## Wymaganie funkcjonalne
1. typy
Expand All @@ -42,6 +44,7 @@ Możliwe jest importowanie zawartości innych skryptów za pomocą instrukcji `p
* obsługa literałów całkowitoliczbowych w formie dziesiętnej (np. `3424`), szesnastkowej (np. `0xaf`), ósemkowej (np. `0x644`) oraz dwójkowej (`0b101011`)
* obsługa literałów zmiennoprzecinkowych z opcjonalną częścią ułamkową, ale nie całkowitą (np. `25.`, ale nie `.1234`) oraz wsparciem dla notacji naukowej bez znormalizowanej mantysy (np. `12.34e15`)
* operatory: znaku (`+`, `-`), dodawania (`+`), odejmowania (`-`), mnożenia (`*`), dzielenia (`/`), reszty z dzielenia (`%`), potęgowania (`^`)
* przepełnienie podczas operacji na liczbach całkowitych nie jest zgłaszane
3. obsługa operacji znakowych
* typ `string`
* wieloliniowe literały ograniczone cudzysłowami wspierające sekwencje ucieczki z wykorzystanie znaku `\`
Expand All @@ -60,17 +63,19 @@ Możliwe jest importowanie zawartości innych skryptów za pomocą instrukcji `p
* instrukcja warunkowa `if`
* opcjonalne części `elif` (wiele wystąpień) oraz `else` (jedno wystąpienie)
* dla `if` oraz `elif` wymagane jest zdefiniowanie warunku w nawiasach
* warunek jest spełniony, jeśli jego wartość rzutowana do typu `bool` wynosi `true`
7. instrukcje pętli
* instrukcja pętli zakresowej `for`
* pozwala na zadeklarowanie niemutowalnego licznika (o wybranej nazwie) inkrementowanego wedle specyfikacji zakresu; deklaracja ta jest opcjonalna (pętla wykona się poprawną ilość razy bez konieczności deklaracji)
* specyfikacja zakresu umieszczona jest w nawiasach po słowie kluczowym `for`, działa analogicznie do konstrukcji `range` z języka Python: możliwe jest określenie tylko górnej granicy (np. `5`), wartości startowej i górnej granicy (np. `0:5`) lub wartości startowej, górnej granicy i kroku inkrementacji (np. `0:5:2`)
* instrukcja pętli warunkowej `while`
* "klasyczna" postać - wymaga podania w nawiasach po słowie kluczowym `while` jakiegoś warunku ewaluowanego przed każdą iteracją
* warunek jest spełniony, jeśli jego wartość rzutowana do typu `bool` wynosi `true`
* przerwanie wykonania
* słowo kluczowe `break` pozwala na bezwarunkowe przerwanie wykonania obu typów pętli
* słowo kluczowe `break_if` pozwala na warunkowe przerwanie wykonania obu typów pętli - warunek należy podać w nawiasach
8. funkcje
* defiowanie funkcji anonimowych z użyciem słowa kluczowego `functi`, po którym następuje lista parametrów i ciało funkcji (blok)
* defiowanie funkcji anonimowych z użyciem słowa kluczowego `functi`, po którym następuje lista parametrów i ciało funkcji
* funkcje anonimowe mogą być przypisane do zmiennej/stałej
* funkcje anonimowe mogą przechwytywać zmienne (mechanizm domknięć), ale nie mogą ich modyfikować
* wywołanie funkcji możliwe jest z użyciem nawiasów, w których podane są argumenty, możliwe rekursywne wywołania
Expand All @@ -79,15 +84,13 @@ Możliwe jest importowanie zawartości innych skryptów za pomocą instrukcji `p
* parametry funkcji można określić słowem kluczowym `const` - nie będzie się dało wtedy modyfikować ich wartości (domyślnie są mutowalne)
* przerwanie wykonania funkcji można wymusić z użyciem słowa kluczowe `return`, po którym następić może wyrażenie stanowiące wartość zwrotną
9. obsługa błędów
* z użyciem domyślnej implementacji: wypisywanie błędów w określonym formacie (zawierającym pozycję, oznaczenie błędu i dodatkowe dane) na standardowy strumień błędów `stderr` na każdym etapie działania aplikacji (błędy znakowe, składniowe, semantyczne, czasu uruchomienia)
* przykładowe błędy:
* znakowe - nieoczekiwany znak (w szczególności ETX)
* składniowe - nieprawidłowa instrukcja, nieoczekiwany token (w szczególności ETX), przekroczenie zakresu literału
* semantyczne - przypisanie wartości do zdefiniowanej wcześniej stałej, nieznany identyfikator, brak przypisania przy definicji stałej
* czasu uruchomienia - dzielenie przez zero, `null` podany w miejsce parametru nieopcjonalnego, przekroczenie zakresu zmiennej, brak wymaganej przestrzeni nazw
* wystąpienie błędu oznacza przerwanie działania bieżącego programu (w przypadku trybu REPL nie powinno to kończyć działania interperetera)
* w przypadku błędów składniowych należy pominąć wszelkie tokeny aż do rozpoczęcia następnej instrukcji i kontynuować sprawdzanie, by użytkownik mógł zapoznać się z możliwie pełną liczbą błędów od razu
* wygodny byłby mechanizm wyjątków z użyciem słów kluczowych `try` i `catch` z możliwością definiowania własnych klas błędów, nie jest to jednak obecnie zaplanowane (wiązałoby się z potencjalnym wprowadzeniem mechanizmu dziedziczenia, wsparcia dla OOP, itd.)
* z użyciem domyślnej implementacji: wypisywanie błędów w określonym formacie (zawierającym pozycję, oznaczenie błędu i dodatkowe dane) na standardowy strumień błędów `stderr` (lub inny wybrany) na każdym etapie działania aplikacji (błędy leksykalne, składniowe, czasu uruchomienia)
* zaimplementowane błędy:
* leksykalne - nieoczekiwany znak (w szczególności koniec strumienia), przekroczenie maksymalnej długości tokena (komentarza, łańcucha znaków lub liczby), nieprawidłowy przedrostek liczbowy, brakująca część liczby (wykładnik, po przedrostku), nieznany token
* składniowe - nieoczekiwany token (w szczególności ETX), wyrażenie lub instrukcja (w miejscu innego tokena, wyrażenia, instrukcji), przekroczenie zakresu liczby całkowitej, niepoprawne użycie gałęzi `default` w dopasowaniu wzorca (podwojenie, nieumieszczenie na ostatniej pozycji), brak wartości początkowej dla zmiennej niemutowalnej, użycie tej samej nazwy parametru więcej niż raz
* czasu uruchomienia - przypisanie wartości do zdefiniowanej wcześniej stałej, nieznany identyfikator, dzielenie przez zero, `null` podany w miejsce parametru nieopcjonalnego, ponowna inicjalizacja zmiennej, nieprawidłowe rzutowanie, użycie instrukcji `return` poza funkcją, użycie instrukcji `break` poza pętlą, wywołanie funkcji z nieprawidłową liczba argumentów, wyrażenie przyjmujące wartość `null` w definicji zakresu pętli `for`, nieprawidłowa l-wartość w przypisaniu
* wystąpienie błędu czasu uruchomienia oznacza przerwanie działania interpretera i ograniczenie się do sparsowania reszty tekstu (nie dotyczy to trybu REPL, gdzie błędy czasu uruchomienia są ignorowane)
* w przypadku niemożliwych do rozwiązania błędów składniowych pomijane są wszelkie tokeny aż do rozpoczęcia następnej instrukcji i kontynuować sprawdzanie, by użytkownik mógł zapoznać się z możliwie pełną liczbą błędów od razu
10. obsługa operacji logicznych
* typ `bool`
* obsługa literałów: `true`, `false`
Expand All @@ -99,7 +102,7 @@ Możliwe jest importowanie zawartości innych skryptów za pomocą instrukcji `p
* sprawdzenie typu (operatory `is`, `is not`)
* porównanie z literałem (operatory `==`, `!=`, `<`, `<=`, `>`, `>=`)
* spełnienie predykatu (poprzez podanie nazwy jednoargumentowej funkcji)
* warunki można grupować za pomocą nawaisów oraz słów kluczowych: `and`, `or`
* warunki można grupować za pomocą nawiasów oraz słów kluczowych: `and`, `or`
12. obsługa opcjonalności
* każda zmienna może przyjąć wartość `null` (`null` jest osobnego typu)
* najprostszą operacją możliwą do przeprowadzenia na wartości opcjonalnej jest dostarczenie w wyrażeniu wartości "awaryjnej" na wypadek wystąpienia nulla za pomocą operatora binarnego `??`
Expand All @@ -108,26 +111,39 @@ Możliwe jest importowanie zawartości innych skryptów za pomocą instrukcji `p
## Wymagania niefunkcjonalne

1. Lekser i parser powinny działać na tyle szybko i sprawnie, by możliwe było wyświetlanie informacji np. o błędach składniowych na żywo w trakcie pisania programu.
2. Interpreter powinien być odporny na błędy - nie zawieszać się, nie przerywać nagle działania.
2. Interpreter w trybie REPL powinien być odporny na błędy - nie zawieszać się, nie przerywać nagle działania.

## Sposób uruchomienia

Interpreter będzie dostarczony w postaci programu konsolowego. Uruchomiony bez argumentów, będzie pobierał dane ze standardowego strumienia wejściowego. Jako argumenty podać można natomiast pliki skryptowe, które zostaną wczytane i wykonane. W takim przypadku standardowe wejście pozostanie nieczynne.
Interpreter jest dostarczony w postaci programu konsolowego. Uruchomiony bez argumentów, pobiera dane ze standardowego strumienia wejściowego. Jako argumenty podać można natomiast plik skryptowy, który zostanie wczytany i wykonany. W takim przypadku standardowe wejście pozostanie nieczynne.

Do zbudowania i uruchomienia programu wymagane jest środowisko .NET w wersji >=6.
Po wejściu do katalogu Toffee należy uruchomić polecenie `dotnet run`.

## Architektura

System składał się będzie z następujących warstw:
System składa się z następujących warstw:
* skaner znaków (leniwa generacja znaków) - śledzenie pozycji, unifikacja znaków nowej linii,
* analizator leksykalny (leniwa generacja tokenów),
* analizator składniowy - parser typu RD generujący hierarchię obiektów,
* analizator składniowy - parser typu RD generujący abstrakcyjne drzewo składniowe (leniwa generacja instrukcji),
* interpreter wygenerowanego drzewa.

Dodatkowo zaimplementowane będą różne klasy do obsługi błędów, logowania, itp.
Dodatkowo zaimplementowane są różne klasy pomocnicze:
* katalog CommandLine - nadzorowanie uruchomienia interpretera (tryb REPL lub nie),
* katalog ErrorHandling - obsługa błędów,
* katalog Running/Operations - operacje czasu uruchomienia (np. arytmetyczne czy rzutowanie),
* katalog Running/Functions - interfejs funkcji i funkcje natywne,
* Running/EnvironmentStack.cs - klasy związane z zarządzaniem zakresem zmiennych i samymi zmiennymi,
* SyntacticAnalysis/CommentSkippingLexer.cs - klasa opakowująca interfejs leksera w celu pominięcia komentarzy,
* Running/AstPrinter.cs - drukarz drzewa składniowego,
* klasy mapujące sekwencje znaków na tokeny, tokeny na operatory, literały, czy typy

## Testowanie

Analizator leksykalny oraz składniowy pracują na zasadzie konsumpcji kolejnych znaków (lub tokenów) i generowania rezultatu, który powinien być deterministyczny, a więc i możliwy do sprawdzenia pod kątem poprawności (np. poprzez proste porównanie sekwencji).

Komponenty systemu testowane będą jednostkowo, niezależnie dzięki wykorzystaniu w implementacji podejścia obiektowego (możliwe będzie podstawienie obiektu np. leksera z użyciem atrapy).
Komponenty systemu (obecnie skaner, lekser i parser) testowane są jednostkowo, niezależnie dzięki wykorzystaniu w implementacji podejścia obiektowego (możliwe będzie podstawienie obiektu np. leksera z użyciem atrapy).

Poza przykładami mającymi zadziałać poprawnie testowane są przypadki brzegowe, np. nagłe przerwanie strumienia wejściowego, nieprawidłowa sekwencja znaków/tokenów.

Oczywiście testowane będą, poza przykładami mającymi zadziałać poprawnie, przypadki brzegowe, np. nagłe przerwanie strumienia wejściowego, dzielenie przez zero i wszelkie błędy, zaproponowane wyżej lub nie.
W testu interpretera zastosowane są testy integracyjne, podające na wejście tekst instrukcji i oczekujące określonego wyjścia.
Loading

0 comments on commit 0d823e5

Please sign in to comment.