Skip to content

MX1010A/TKOM_PW

Repository files navigation

TKOM | Anton Basan


Projekt wstępny

Język programowania ogólnego przeznaczenia.

Opis zakładanej funkcjonalności

Podstawowe typy danych i operacje

Liczby całkowite (int):

  • Przechowują liczby całkowite od -2147483648 do 2147483647.

Liczby zmiennoprzecinkowe (float):

  • Przechowują liczby zmiennoprzecinkowe od -3.40282347E+38 do 3.40282347E+38.
  • Operacje arytmetyczne na liczbach zmiennoprzecinkowych obejmują te same operacje co dla liczb całkowitych.

Wartości logiczne (bool):

  • Przechowują jedną z dwóch wartości logicznych: true lub false, stosowane głównie w operacjach warunkowych.

Ciągi znaków (string):

  • Przechowują sekwencję znaków w ilości od 0 do 2147483647, umożliwiając operacje na tekstach.

Typy użytkownika (class type):

  • Użytkownicy mogą definiować własne typy danych, tworząc klasy, które zawierają właściwości i metody, pozwalając na tworzenie złożonych struktur danych.

Operacje i operatory arytmetyczne:

  • Dodawanie: +
  • Odejmowanie: -
  • Mnożenie: *
  • Dzielenie: /
  • Dzielenie całkowite: /.
  • Dzielenie modulo (reszta z dzielenia): %
var int a = 10;
var float b = 5.5;

var int sum = a + 2; // 12
var float result = b * 2.0; // 11.0

Operatory przypisania:

  • Przypisanie: =

Operatory relacji:

  • Równość: ==
  • Nierówność: !=
  • Większe od: >
  • Mniejsze od: <
  • Większe bądź równe: >=
  • Mniejsze bądź równe: <=

Operatory logiczne:

  • Operator && – zwraca true, jeśli oba warunki są spełnione.
  • Operator || – zwraca true, jeśli co najmniej jeden warunek jest spełniony.
  • Operator ! – zwraca wartość przeciwną (true zamienia na false i odwrotnie).
var bool isEven = (a % 2 == 0);
var bool inRange = (a > 5 && a < 15);

Priorytety i łączność operatorów:

Standardowe reguły matematyczne obowiązują w przypadku priorytetów operacji arytmetycznych. Nawiasy mają pierwszeństwo, umożliwiając wykonywanie operacji w określonej kolejności niezależnie od domyślnego priorytetu operatorów.

Komentarze

Komentarze zaczynają się od znaków // i kończą się nową linią, są ignorowane przez interpreter. Służą do dodawania notatek lub wyjaśnień w kodzie.

Zmienne i czas życia

Zmienne powinny być tworzone z deklaracją typu i mają dynamicznie przypisywaną wartość. Czas życia zmiennej jest związany z zakresem, w którym została zdefiniowana.

func exampleFunc() : void {
  var int funcVar = 10;

  if(funcVar == 10)
  {
      var int ifVar = funcVar - 5;
  }

  output(ifVar) //Semantic error: Cannot find symbol "ifVar"
}

Semantyka obsługi zmiennych

Jest to język o typowaniu silnym, czyli operacje na różnych typach danych są ściśle określone i nie ma automatycznych konwersji. Jednakże, istnieje możliwość użycia funkcji rzutowania typów t.j. to_int() oraz to_float().

Wszystkie typy danych języka są mutowalne.

Instrukcje warunkowe i pętle

W języku istnieją instrukcje warunkowe (if, elif, else) oraz pętla while. Przykładowo:

var int x = 5;

if (x > 0) {
  output("x is positive");
} elif (x == 0) {
  output("x equals zero");
} else {
  output("x is negative");
}
var int counter = 3;
while (counter > 0) {
  output("Counting down: ");
  output(counter);
  counter = counter - 1;
}

Funkcje

Można definiować i wywoływać własne funkcje za pomocą słowa kluczowego func. Typ zwracanej wartości powinien być zdefiniowany przez programistę (func float). Argumenty funkcji są przekazywane przez wartość lub referencję, w zależności od typu danych (bool, int, float - wartość; string, obiekty class - referencja).

func calculateArea(float radius) : float {
  var float pi = 3.14159;
  return pi * radius * radius;
}

Funkcje wbudowane dostarczają dodatkowe możliwości, takie jak output(), input(), int(), float() itp..

Rekursja

Język obsługuje rekursję, czyli wywoływanie funkcji przez samą siebie.

func factorial(int number) : int {
  if (number <= 1) {
    return 1;
  } else {
    return number * factorial(number - 1);
  }
}

Klasy

Można tworzyć własne klasy, które są szablonami dla obiektów. Klasy mogą zawierać właściwości (zmienne) i metody (funkcje). Jest możliwa agregacja obiektów klas.

class Person {
  var string _name = "Unknown";
  var string _surname = "Unknown";
  var string _id = "000000";

  func Person(string name, string surname, string id) : Person {
    _name = name;
    _surname = surname;
    _id = id;
  }
}

Agregacja

W języku można również stosować agregację, co oznacza, że jeden obiekt może zawierać inne obiekty jako swoje składniki.

class Address {
  var string _city = "Warsaw";
  var string _street = "Nowowiejska";

  func Address(string city, string street) : Adress {
    _city = city;
    _street = street;
  }
}

class Student : Person {
  var string _name = "Unknown";
  var string _surname = "Unknown";
  var string _student_id = "000000";
  var Address _address = Address();

  func Student(string name, string surname, string id, string student_id, Address address) : Student {
    _name = name;
    _surname = surname;
    _student_id = student_id;
    _address = address;
  }
}

Tworzenie i używanie obiektów:

Tworzenie obiektu klasy odbywa się za pomocą konstruktora (użytkownika, domyślnego albo kopiującego). Używa się konstruktor domyślny, jeżeli nie zostały podane potrzebne parametry lub inny nie został zaimplementowany przez programistę:

var Address address = Address("Warsaw", "Main Street");
var Student student = Student("Anton", "Basan", "324456", address);

student._adress._street = "Starowiejska";
output(student._street);

Formalna specyfikacja i składnia

Postać EBNF realizowanego języka

program             = { class_def | function_def } ;

class_def           = "class", identifier, class_block;

function_def        = "func", func_type, identifier, "(", parameters ")", block;

parameter_list      = [ parameter, { ",", parameter } ] ;

parameter           = var_type, identifier ;

func_type           = var_type | "void" ;

var_type            = "int" | "float" | "string" | "bool" | identifier ;

class_block         = "{", { var_def_statement | function_def }, "}";

block          = "{", { statement }, "}" ;

statement           = 
                      (
                        var_def_statement |
                        assign_statement |
                        function_call |
                        if_statement |
                        while_statement |
                        throw_statement |
                        return_statement
                      );

expression          = [ "-" | "!" ], identifier | int_literal | float_literal | string_literal | bool_literal | identifier_or_call | "(", conditional expression, ")" ;

conditional_expr    = logical_or_expr, { "&&", logical_or_expr} ;

logical_and_expr    = logical_expr { "||", logical_expr } ;

logical_expr        = arithmetical_expr, [ "<" | ">" | "<=" | ">=" | "==" | "!=", arithmetical_expr ] ;

arithmetical_expr   = multiplicational_expr, { ( "+" | "-" ), multiplicational_expr } ;

multiplicational_expr = term, { ( "*" | "/" | "/." | "%" ), term } ;

term                = ["-" | "!"], (identifier_or_call | "(", expression, ")" | literal);

data_access         = identifier_or_call, {".", identifier_or_call} ;

identifier_or_call  = identifier, ["(", argument_list, ")"];

if_statement        = "if", "(", conditional_expr ")", block, { "elif", "(", conditional_expr ")", block }, ["else", block] ;

while_statement     = "while", "(", conditional_expr, ")", block ;

return_statement    = "return", [ expression ], ";" ;

var_def_statement   = var_type, assign_statement;

argument_list       = [ expression, { "," , expression } ] ;

assign_statement    = data_access, ["=", expression] }, ";" ;

throw_statement     = "throw", [string_literal], ";" ;

letter              = "a" | "b" | "c" | "d" | ... | "x" | "y" | "z" ;

first_digit         = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;

digit               = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;

int_literal         = first_digit, { digit } ;

float_literal       = digit, ".", { digit } ;

string_literal      = '"', { letter | digit | special_char }, '"' ;

bool_literal        = "true" | "false" ;

identifier          = letter, { letter | digit } ;

Obsługa błędów

Błędy są obsługiwane przez każdy z interfejsów:

  • Lexer - błędy leksykalne (np. "Lexical error: Unexpected operator! 1:1")
  • Parser - błędy składniowe (np. "Syntax error: Semicolon expected! 1:1")
  • Interpreter - błędy semantyczne (np. "Semantic error: Symbol "variable" already exists. line: 1")

Pliki wejściowe i strumienie danych

  • Plik źródłowy: Zawiera kod w języku zdefiniowanym przez interpreter.
  • Strumienie danych: Pozwala na interakcję z programem poprzez wprowadzanie danych wejściowych i odbieranie danych wyjściowych.

Sposób uruchomienia. Działanie wejść i wyjść

Uruchomienie kodu odbywa się z tekstowego pliku źródłowego przez podaną ścieżkę do niego. Do pobierania danych od użytkownika z klawiatury służy funkcja input(). Typ pobranych danyh ustala interpretor:

var string name = input();
output(name);

Funkcja input() wyświetli wiadomość podaną jako argument i zatrzyma wykonanie programu, czekając na wprowadzenie danych przez użytkownika. Po naciśnięciu klawisza Enter, wprowadzone dane będą zwrócone jako string.

Do wyświetlania danych dla użytkownika używa się funkcji output():

output("Hello, world!");

Funkcja output() służy do wyświetlania tekstu lub wartości różnych typów danych. Można podać wiele argumentów oddzielonych przecinkami, a output() wyświetli je w jednej linii, domyślnie oddzielając spacją.

Analiza wymagań funkcjonalnych i niefunkcjonalnych

Wymagania funkcjonalne

  • Obsługa podstawowych typów danych liczbowych:
    • operacje matematyczne o różnym priorytecie wykonania, obsługa nawiasów
    • operacje logiczne i porównania o różnym priorytecie wykonania, obsługa nawiasów
  • Obsługa typu znakowego
  • Obsługa komentarzy
  • Tworzenie zmiennych, przypisywanie do nich wartości i odczytywanie ich
  • Instrukcje warunkowe
  • Instrukcje pętli
  • Wywołanie i definiowanie własnych funkcji (ze zmiennymi lokalnymi)
  • Rekursywne wywołania funkcji
  • Tworzenie klas i wykorzystanie ich obiektów
  • Agregacja klas

Wymagania niefunkcjonalne

  • Czytelnność języka
  • Wygodność dla programisty

Opis sposobu realizacji (w języku C#)

Analiza leksykalna

Opis: Pierwszy etap przetwarzania, podczas którego sekwencja znaków jest analizowana na podstawie zdefiniowanych tokenów (np. identyfikatory, słowa kluczowe, operatory, liczby).

Realizacja: Wykorzystanie analizatora leksykalnego do identyfikacji i zwracania tokenów, gdzie każdy token reprezentuje określony rodzaj leksemu w języku.

Analiza składniowa (Parsowanie)

Opis: Konwertuje ciąg tokenów na strukturę drzewa składniowego zgodnie z regułami gramatyki języka.

Realizacja: Użycie parsera do sprawdzania poprawności składniowej i tworzenia drzewa składniowego zgodnie z zdefiniowaną gramatyką języka.

Analiza semantyczna

Opis: Proces weryfikacji znaczenia i poprawności programu, takie jak deklaracje zmiennych, typy danych, kontekst zmiennych i ich używanie.

Realizacja: Wykorzystanie analizy semantycznej do zapewnienia spójności w programie, sprawdzając typy danych, rozpoznając zasięg zmiennych i wykonywanie operacji semantycznych.

Wykonywanie

Opis: Interpretacja skompilowanego drzewa składniowego i wykonanie operacji zgodnie z zaimplementowanym zachowaniem języka.

Realizacja: Wykorzystanie wygenerowanego drzewa składniowego do interpretacji programu, wykonywanie odpowiednich operacji w zależności od rozpoznanych struktur w drzewie. Błąd wejścia/wyjścia: "Input/Output error: Unable to open file 'file_name'."

Struktury danych

  • Tokeny: Przechowywane w listach lub strukturach danych do przechowywania informacji o leksemach.
  • Drzewo składniowe: Zwykle realizowane jako hierarchiczna struktura danych, na przykład obiekty reprezentujące węzły drzewa.
  • Kontekst zmiennych: Używane do śledzenia zasięgu zmiennych, ich typów i wartości.
  • Bufor pamięci: Do przechowywania wartości pośrednich lub wyników operacji.

Pośrednie Formy

  • AST (Abstract Syntax Tree - Drzewo Składniowe): Struktura danych reprezentująca skompilowany program w formie hierarchicznego drzewa, używana do interpretacji.

Interfejsy

  • Lexer (Analizator leksykalny): Interfejs do przetwarzania tekstu na tokeny.które Pozwala na pobieranie, podglądanie lub przesuwanie się do następnego tokenu w tekście. Tokeny mogą być reprezentowane przez klasy przechowujące informacje o rodzaju tokenu oraz jego wartości.
  • Parser (Analizator składniowy): Interfejs do przekształcania tokenów w drzewo składniowe zgodnie z gramatyką języka.
  • Interpreter (Wykonywanie): Interfejs do interpretacji drzewa składniowego i wykonania operacji.

Opis sposobu testowania

Testy składniowe:

  • Sprawdzenie poprawności składni języka. Na przykład, czy interpreter poprawnie interpretuje deklaracje zmiennych, struktury kontrolne (if, else, while), instrukcje warunkowe i pętle.

Testy semantyczne:

  • Upewnienie się, czy język działa zgodnie z oczekiwaniami w zakresie operacji arytmetycznych, logicznych, operacji na typach danych (np. sprawdzanie, czy dodawanie dwóch liczb działa poprawnie).
  • Testowanie funkcji wbudowanych i ich poprawności.

Testy obsługi błędów:

  • Sprawdzenie reakcji języka na niepoprawne działania, na przykład podanie złej składni lub typu danych.

Testy integracyjne:

  • Testowanie większych fragmentów kodu lub programów, aby upewnić się, że różne części języka współpracują ze sobą poprawnie.

Testy zewnętrzne:

  • Testy, które porównują wyniki tworzonego języka z wynikami innego języka, jeśli jakiś kod jest równoważny i może zostać przetłumaczony na oba języki.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages