Celem projektu jest stworzenie języka BitScope, zaimplementowanie interpretera języka programowania ogólnego przeznaczenia z typem maski bitowej, z obiektowymi atrybutami i metodami. Stworzony przeze mnie język będzie statycznie, silnie typowany z mutowalnymi zmiennymi, przekazywanymi przez referencję.
Charakterystycznym elementem jezyka będzie typ maski bitowej (bmask), reprezentowany jako klasa z obiektowymi atrybutami i metodami. Cechy typu:
- Reprezentacja - maska bitowa będzie reprezentowana jako tablica wartości logicznych
TrueiFalse, w której nie występuje bit znaku. To oznacza że wszystkie bity w masce przeznaczone są na wartość liczby. Dla zwiększenia czytelności wypis maski jako string następuje jako wypis tablicy1i0reprezentujących odpowiednio wartościTrueiFalsew masce - Konwencja Little Endian - w reprezentacji maski bitowej, najmłodszy bit znajduję się na najmnijeszym indeksie w tablicy
- Wielkości masek - użytkownik jest w stanie stworzyć maski o dowolnej wielkości
nie większej niż 32deklarując swój wybór poprzez mask(wielkość maski wyrażaona liczbą całkowitą) - Podmaski — z jednej dużej maski, można wyodrębnić podmaski, odpowiadające za konkretne funkcjonalności. Tworzy się wtedy powiązanie bitów w dwóch różnych maskach za pomocą stworzenia podmaski będącej atrybutem danej maski, lub poprzez swtorzenie nowego obiektu z odpowiającymi bitami.
- Dynamiczna aktualizacja — kiedy bity danej maski ulegają zmianie, odpowiadające tym bitom bity w powiązanych podmaskach, również ulegną zmianie.
- Nadawanie nazw - danej masce, submasce lub pojedyńczemu bitowi można
nadać nazwęi posługiwać się nią upraszczając zarządzanie dużą maską bitową - Operacje bitowe - jest możliwość wykonania operacji
conjunction,disjunction,exclusiveorazinversena dwóch maskach bitowych nierównej długości - Domyślna wartość - po inicjalizacji obiektu
bmaskwszytskie jej bity są zanegowane, domyślna wartość przypisana przez konstruktor domyślny to8 - Konwersja - typowi
bmaskbędzie można przypisać wartość całkowitą, próby przypisania innych typów zakończone będą odpowiednim błędem
Obsługiwanymi typami zmiennych będą:
bool- typ danych logicznyTruelubFalsestr- typ danych do przechowywania ciągu znakówint- typ danych do przechowywania liczb całkowitychfloat- typ danych do przechowywania liczb zmiennoprzecinkowychbmask- typ danych reprezentujący maskę bitową
| Typ | Minimalna wartość | Maksymalna wartość | Minimalna długość | Maksymalna długość |
|---|---|---|---|---|
bool |
False |
True |
- | - |
str |
- | - | 0 znaków | 512 znaków |
int |
-231 = -2147483648 | 231 - 1 = 2147483647 | - | - |
float |
-3.4 × 10-38 | 3.4 × 1038 | - | - |
bmask |
0 | 232 - 1 | 1 bit | 32 bitów |
Precyzja typu float to Single Precision, obsługujący 6 cyfr znaczących.
W języku istnieje możliwość przypisania masce bitowej liczby w postacji binarnej używając prefiks $, wyrażenie binarne jest w konwencji Big-Endian co oznacza, że najmniej znaczący bit znajduję się na skrajnie prawej pozycji w wyrażeniu po prefiksie $
bmask mask1 = mask(8);
mask1 = $00011100; # Przypisanie liczby binarnej do maski bitowej
Ograniczeniem liczby w tej reprezentacji jest 32 znaków po znaku $, ponieważ 32 to maksymalny rozmiar typu bmask.
| Słowo | Opis |
|---|---|
AND |
logiczny AND pomiędzy dwoma wyrażeniami |
OR |
logiczny OR pomiędzy dwoma wyrażeniami |
if |
instrukcja warunkowa if(warunek) {} |
elif |
instrukcja warunkowa elif(warunek) {} występująca po if |
else |
instrukcja warunkowa else{} występująca po elif |
while |
pętla while(warunek) {} |
break |
instrukcja przerwania pętli |
# |
znak komentarza jednoliniowego |
bmask |
odpowiada konstruktorowi typu bmask,mask(rozmiarMaski) |
; |
znak końca instrukcji |
{} |
znaki definiowania bloku kodu |
+ |
operator dodawania na typach int, float |
- |
operator odejmowania na typach int, float |
/ |
operator dzielenia zmiennoprzecinkowego na typach int, float |
* |
operator mnożenia na typach int, float |
= |
operator przypisaniach na typach int, float, bool, bmask, str |
== |
operator porównania na typach int, float, bool, bmask, str |
return |
instrukcja służąca do zwrócenia wyniku funkcji oraz zakończenia jej działania |
> |
operator porównania do typów int, float, zwraca True jeżeli wartość po lewej stronie od operatora jest większa od wartości po lewej |
>= |
operator porównania do typów int, float, zwraca True jeżeli wartość po lewej stronie od operatora jest większa lub równa wartości po lewej |
< |
operator porównania do typów int, float, zwraca True jeżeli wartość po lewej stronie od operatora jest mniejsza od wartości po lewej |
<= |
operator porównania do typów int, float, zwraca True jeżeli wartość po lewej stronie od operatora jest mniejsza lub równa wartości po lewej |
!= |
operator nierówności do typów int, float zwraca True jeżeli wartość po lewej stronie od operatora jest różne od wartości po lewej |
() |
znaki odpowiadające za grupowanie wyrażeń, nadając wyższy priorytet, wywoływania funkcji z przekazywania do nich argumentów |
[] |
znaki odpowiadające za określanie typu wynikowego funkcji, mogą przyjmować wartości: int, float, bool, str, bmask, void |
inverse |
operator negacji na typie bmask |
conjunction |
operator logicznego AND na dwóch obiektach typu bmask |
disjunction |
operator logicznego OR na dwóch obiektach typu bmask |
exclusive |
operator logicznego XOR na dwóch obiektach typu bmask |
\ |
znak modyfikujący powodujący inną niż domyślna interpretację kolejnego znaku |
- |
znak negacji na typie int, float |
| Priorytet | Operator | Asocjatywność | Typ |
|---|---|---|---|
| 8 | NOT, inverse, - | prawostronna | unarna |
| 7 | conjunction, disjunction, exclusive | lewostronna | binarna |
| 6 | /, * | lewostronna | binarna |
| 5 | -, + | lewostronna | binarna |
| 4 | ==, !=, >=, >, <=, < | lewostronna | binarna |
| 3 | AND | lewostronna | binarna |
| 2 | OR | lewostronna | binarna |
| 1 | = | prawostronna | binarna |
Język będzie statycznie typowany, zatem typy zmiennych będą musiały być jawnie określone przy ich deklaracji. Przykłady użycia:
bool a = True;
bool b = False;
str c = "cat";
int d = 1;
float e = 1.5;
bmask f = 54321;
Język będzie silnie typowany, zatem będzie konieczność używania operatora rzutowania, poniżej znajdują się zasady konstruowania wyrażeń:
| Przykład | Zastosowane typy | Czy obsługiwane |
|---|---|---|
| 1 + 1.5 | int + float | NIE |
| 1 + 2 | int + int | TAK |
| 1.2 + 2 | float + int | NIE |
| True + 2 | bool + int | NIE |
| True.toInt() + 2 | int + int | TAK |
| False + 2.5 | bool + float | NIE |
| False.toFloat() + 2.5 | float + float | TAK |
| "Adam" + 5 | str + int | NIE |
| "Adam" + 5.5 | str + float | NIE |
| bmask mask1 = "Adam" | bmask = str | NIE |
| bmask mask1 = 45.23 | bmask = float | NIE |
| bmask mask1 = 45.23.toInt() | bmask = błędne zastosowanie .toInt() | NIE |
| bmask mask1 = True | bmask = bool | NIE |
| bmask mask1 = True.toInt() | bmask = int | TAK |
| "abc".toInt() | konwerjsa string na Int | NIE |
| "abc".toFloat() | konwersja string na float | NIE |
| 1.2.toInt() + 2 | bezpośrednia konwersja float na int + int | NIE |
| "Adam" + 5.5.toStr() | bezpośrednia konwersja float na str + str | NIE |
Dostępne są poniższe metody konwersji typu (gdzie x to przykładowy obiekt):
| Metoda | Opis |
|---|---|
| x.toInt() | konwersja obiektu x do typu całkowitoliczbowego |
| x.toFloat() | konwersja obiektu x do typu zmiennoliczbowego |
| x.toStr() | konwersja obiektu x do napisu |
| x.toBool() | konwersja obiektu x do typu logicznego |
Ich celem jest przekształcenie obiektów na inne typy:
float a = 3.123;
int b = a.toInt();
# konwersja liczby zmiennoprzecinkowej do całkowitej
bmap mask1 = bmap(8);
str mask1Converted = mask1.value.toStr();
# konwersja mapy bitowej na napis
float c = 54.32;
str d = c.toStr();
# konwersja liczby zmiennoprzecinkowej na napis
int e = 20;
str f = e.toStr();
# konwersja liczby całkowitej na napis
int g = 10;
float h = g.toFloat();
# konwersja liczby cakowitej do zmiennoprzecinkowej
bool k = g.toBool();
# konwersja liczby całkowitej na wartośc bool, liczy dodatnie odpowiadają wartości True, natomiast niedodatnie wartośi False
bool k2 = "ABBA".toBool();
# konwersja napisu na wartość bool, niepusty napis zwróci wartość po konwersji True, natomiast pusty napis zwróci False
bool l = a.toBool();
# konwersja liczby zmiennoprzecinkowej na wartośc bool, liczy dodatnie odpowiadają wartości True, natomiast niedodatnie wartośi False
str n = 4.12.toStr();
# konwersja liczby zmiennoprzecinkowej na napis
bmask mask1 = mask(8);
int maskValue = mask1.value.toInt();
# odczytanie wartości decymalnej przechowywanej na masce bitowej
bool maskValue4 = mask2.toBool()
# W wyniku tej operacji jeżeli maska przechowuje wartość dodatnią zwrócona zostanie wartość True, jeżeli maska przechowuje 0 to zwróci False
Błędne próby konwersji:
int l = "ABBA".toInt(); # Błąd, nie można przekształcić napisu w liczbę całkowitą
float o = "ABBA".toFloat() # Błąd, nie można przekształcić napisu w liczbę zmiennoprzecinkową
bmask mask2 = mask(8);
int maskValue2 = mask2.toStr(); # Błąd, nie można przekształcić maski bitowej na tekst
float maskValue3 = mask2.toFloat() # Błąd, maska bitowa przechowuje tylko typ całkowity
Powyższe metody nie zmieniają obiektu, na którym są wykonywane.
Zmienne są mutowalne zatem ich wartość, może ulec wielokrotnie zmianie:
float a = 1.0;
a = 1.5;
a = 2.0;
Zmienne są widoczne w bloku ograniczonymi { }, w którym zostały zadeklarowane:
void newFunction(int a){
if (a < 10){
int y = 5; # Zmienna y jest widoczna tylko w tym bloku if
y = y + a;
}
int b = 15;
b = b + y; # Ta operacje nie jest dozwolona, zmienna y już nie jest widoczna
}
Przekazując zmienną do funkcji domyślnie przekazujemy jej referencję:
void changeA(int a){
a = a + 5;
}
void main(){
int y = 6;
changeA(y); # Zmienna y została przekazana przez referencję
print(y); # wynikiem operacji print będzie 11, ponieważ zmienna y uległa zmianie w funkcji changeA
}
>>>11
Do escape'owania służy znak \, bezpośrednio po nim dozwolone jest umieszczenie znaków: n, t, \ oraz ".
Wszystkie inne znaki spodują błąd: Invalid use of backslash in string
str a = "abba \ abba"; # błąd
str g = "abba \\ abba"; # astąpiło escape'owanie znaku \, zatem program odczyta "abba \ abba" jako jeden napis
str b = "b \n c"; # znak \n zostanie ustany jako znak nowej linii
str c = "c \\n d"; # nastąpiło escape'owanie znaku \, zatem program odczyta "c \n d" jako jeden napis
str d = "d \t e"; # znak \t uznany zostanie jako tabulator
str e = "e \\t f"; # nastąpiło escape'owanie znaku \, zatem program odczyta "e \t f" jako jeden napis
str f = "f \" g"; # nastąpiło escape'owanie znaku \, zatem program odczyta "f " g" jako jeden napis
| Przykład | Efekt |
|---|---|
| float a = 1.2; a.toInt() + 2 -> wyrażenie dodawania będzie miało wartość 3 |
Obcięcie części ułamkowej |
| float b = 1.234; b.toBool() or False -> wyrażenie OR będzie miało wartość True |
rzutowanie typu bool na dodatniego floata spowoduje wartość True, niezależnie jak duży jest zmienne typu float. Niedodatni float zostanie zamieniony na wartość False |
|
Obiekt maski bitowej posiada konstruktor mask. Konstruktor musi mieć pierwszy argument size czyli rozmiar maski i drugi argument name jest opcjonalny.
bmask a; #Konstruktor domyślny tworzący obiekt maski bitowej, 8-bitowy, bez nazwy
bmask a = mask(14); #Konstruktor tworzący maskę 14-bitową bez nazwy
bmask a = mask(14, "Gain"); #Konstruktor tworzący maskę 14-bitową z nazwą "Gain"
Na masce można wykonać metody .toInt(), .toFloat(), .toBool(), .toStr():
bmask mask1 = mask(8);
mask1 = $00000010;
print(mask1.toInt());
>>>2
Przypisywanie wartości dziesiętnej:
bmask mask1 = mask(32);
mask1 = 54321;
print(mask1);
>>>[1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1]
Przypisywanie wartości bitowej:
bmask mask3 = mask(8);
mask3 = $00001010;
print(mask3.toInt());
>>>10
Typ bmask przyjmuje jedynie liczby całkowite, przypisanie mu wartości float, bool lub str spowoduje błąd.
W przypadku przypisania wartości float, wymagane jest użycie metody .toInt(), jednak aby tego dokonać konieczne jest użycie zmiennej pośredniej:
bmask mask1 = bmask(32);
# mask1 = 54321.45; przypisanie float'a bezpośrednio spowoduje błąd
float a = 54321.45;
# metoda .toInt() działa jedynie na zmiennych, wykonanie jej bezpośrednio na typie float spowoduje błąd
mask1 = a.toInt();
Do każdego bitu w masce bitowej można odwołac się poprzez mask[idx] oraz nadać im nazwy poprzez atrybut name, który domyślnie jest pusty dla każdego bitu w masce.
po nadaniu nazwy danemu bitowi, można się do niego bezpośrednio odwołać:
bmask mask1 = mask(16);
mask1[4].name = "isOpened";
mask.isOpened = True;
if (mask.isOpened.toBool()){
print("Appropriate message");
}
Istnieje również możliwość nadania bitu masce analogicznie poprzez atrybut name:
str main(){
bmask mask1;
mask1.name = "Gain";
return mask1.name;
}
Został tutaj również konstruktor domyślny obiektu BMask.
W ramach maski bitowej, można również wydzielić część jej bitów tworząc podmaske. Możliwe jest utworzenie wielu podmasek w ramach jednej maski oraz dany bit może należeć do więcej niż jednej podmaski:
bmask mask2 = mask(8);
mask2 = 10;
# mask2 -> [0, 0, 0, 0, 1, 0, 1, 0]
mask2.submask([0, 2, 3], "gain");
# bity o indeksach 0, 2 i 3 w mask2 tworzą podmaske o nazwie gain -> [1, 0, 0] czyli wartość 4 dziesiętnie
int gainValue = mask2.gain.toInt();
print(gainValue);
mask2.submask([3, 4, 5], "controlBits");
# bity o indeksach 3, 4 i 5 w mask2 tworzą podmaske o nazwie controlBits -> [0, 0, 1] czyli wartość 1 dziesiętnie
int controlBitsValue = mask2.controlBits.toInt();
print(controlBitsValue);
>>>4
>>>1
Istnieje również możliwość wyodrębnienia części bitów z maski bitowej i stworzenia z nich nowej oddzielnej maski:
bmask maskA = mask(16);
bmask maskB = mask([3, 12, 15], maskA);
# po tej operacji maskB[0] = maskA[3], maskB[1] = maskA[12] oraz maskB[2] = maskA[15]
maskB.append([5, 13], maskA);
# po tej operacji maskA zwiększy swój rozmiar z 3 do 5 bitowej gdzie dodatkowo maskB[3] = maskA[5] i maskB[4] = maskA[13]
maskB.reconfigure([7, 14], maskA);
# możliwa jest konfiguracja mapowania poprzez podanie odpowiadjących indeksów licząc od indexu 0 w maskB
# Teraz maskB[0] = maskA[7] i maskB[1] = maskA[14]
# Natomiast pozostałe mapowanie nie uległo zmianie
bmask maskC = mask(8);
maskB.append([6], maskC);
# Zostało dodane powiązanie bitu o indeksie 6 z maskC
Można również nadać nazwę obiektowi nowej maski, który ma bity powiazane z inną maską:
bmask maskA = mask(16);
bmask maskB = mask([3, 12, 15], maskA, "important");
# po tej operacji obiekt maski ma nazwę "important"
print(maskB.name);
# lub można zmienić nazwę odwołując się po kropce
maskB.name = "notimportant"
print(maskB.name);
>>>important
>>>notimportant
Operacja ta również może być wykonana odnosząc się do nazw poszczególnych bitów:
bmask mask1 = mask(16);
mask1[3].name = "enabled";
mask1[6].name = "opened";
mask1[10].name = "reachable";
bmask mask2 = mask(["enabled", "opened", "reachable"], mask1); # analogicznie można nadać w tym miejscu jako trzeci atrybut nazwę maski
# po tej operacji utworzy się obiekt 3-bitowa maska mask2 z bitami odpowiadającymi bitom w masce mask1
# przypisując indexy do nowej maski mask2 będzie następujące przypisanie mask2[0] -> "enabled", mask2[1] -> "opened", mask2[2] -> "reachable"
print("Initial state: ");
print(mask1);
print(mask2);
mask2.value = 5;
print("Masks after change mask2: ")
print(mask1);
print(mask2);
# możliwa jest zmiana wartości pojedyńczych bitów
mask1[3] = False;
mask1[10] = False;
mask1[6] = True;
print("Masks after change mask1: ");
print(mask1);
print(mask2);
Initial state:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0]
Masks after change mask2:
[0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0]
[1, 0, 1]
Masks after change mask1:
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 1, 0]
Analogicznie do poprzedniego przykładu trzeci argument konstrukora mask nadaje nazwę nowemu obiektowi maski biowej:
bmask maskA = mask(16);
mask1[3].name = "enabled";
bmask maskB = mask(["enabled", 12, 15], maskA, "important"); # po tej operacji obiekt maski ma nazwę "important"
print(maskB.name);
maskB.name = "notimportant" # lub można zmienić nazwę odwołując się po kropce
print(maskB.name);
>>>important
>>>notimportant
# na początku maskA ma podmaski: ["gain", "permissions", "controllable"]
maskA.remove("gain");
# po wykonaniu operacji podmaski maskA to: ["permissions", "controllable"]
Na typie bmask, jest możliwa negacja maski:
bmask mask1 = mask(8);
mask1 = 170;
bmask modifiedMask1 = inverse mask1;
print(mask1);
print(modifiedMask1);
>>>[1, 0, 1, 0, 1, 0, 1, 0]
>>>[0, 1, 0, 1, 0, 1, 0, 1]
Na maskach są dostępne trzy operacje binarne bitowe: conjunction, disjunction, exclusive pomieiędzy dwoma obiekatami typu bmask:
bmask mask1 = mask(8);
bmask mask2 = mask(16);
mask1 = 170 # binarnie 10101010;
mask2 = 43760 # binarnie 1010101011110000;
# Gdy maski są różnych rozmiarów, następuję dopełnienie zerami maski do wielkości maski większej
bmask result1 = mask1 conjunction mask2;
bmask result2 = mask2 disjunction mask2;
bmask result3 = mask1 exclusive mask2;
print(mask1);
print(mask2);
print(result1);
print(result2);
print(result3);
>>>[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0]
>>>[1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0]
>>>[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0]
>>>[1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0]
>>>[1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0]
Dostępny w języku jest pętla while:
while(a > b){
c = c + 10;
}
Dostępne są instrukcje if, elif, else:
if (a > 123){
b = 10;
}elif (a < 56){
b = 100;
}else{
b = 1000;
}
Aby zdefiniować funckję należy na początku określić typ zwracany przez funkcję a następnie podać ewentualne argumenty wywołania. W języku BitScope nie da się przeciążać funkcji.
int myFunction(int a, int b){
return a + b;
}
bmask myFunction2(int c, float d){
bmask mask1 = mask(16);
mask1 = c + d.asInt();
return mask1;
}
str nameSurname(str a, str b){
return a + " " + b;
}
void main() {
print("Hello World!");
str name = "Adam";
str surname = "Smith";
print("Hello " + nameSurname(name, surname));
}
>>>Hello World!
>>>Hello
W języku jest jedynie możliwość komentarzy jednolinijkowych poprzedzonych znakiem kluczowym #:
# comment with valuable information
Do wypisu będzie służyła funkcja print:
print("Hello World!");
>>>Hello World!
(* definicja liczby, cześć całkowita liczby zaczyna się od non_zero_digit, czesc ułamkowa może od zero *)
zero = "0";
positive_digit = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
digit = positive_digit | zero;
letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" |
"J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" |j
"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";
new_line = "\n";
(* typy danych *)
literal_bool = "True" | "False";
literal_int = positive_digit, { digit } | zero;
literal_float = literal_int, ".", digit, { digit };
literal_str = '"', { char }, '"'; ( * możliwy znak escape \ *)
literal_binary = "$", { "0" | "1" };
int_list = literal_int, { ",", literal_int };
expression_list = expression, { ",", expression };
bit_list = "[", expression_list, "]";
type_return = "void" | type_variable;
type_variable = "bool" | "int" | "float" | "str" | "bmask";
(* identyfikar zmiennych lub funckji *)
identifiler = letter, { letter | digit | "_"};
(* operatory *)
operator_bitwise = "conjunction" | "disjunction" | "exclusive";
operator_relation = "==" | "!=" | ">=" | ">" | "<=" | "<";
operator_and = "AND";
operator_or = "OR";
operator_negation = "NOT" | "inverse" | "-";
operator_multiplicative = "*" | "/";
operator_additive = "-" | "+";
(* komentarz jednolinijkowy *)
comment = "#", { char }, new_line;
(* deklaracje *)
declaration_variable = type_variable, identifiler, [ "=", expression ], ";";
declaration_parameter = type_variable, identifiler;
declaration_parameters = [ declaration_parameter, { "," declaration_parameter } ]
declaration_function = type_return, identifiler, "(", declaration_parameters, ")", block ;
(* metody na bmask *)
submask_as_attribute = identifier, ".", "submask", "(", int_list, ",", literal_str, ")", ";"
method_type_conversion = identifier, ( ".", "toInt" | "toFloat" | "toStr" | "toBool" ), "()";
method_value_access = identifier, ".", indetifier, (".", "toInt" | "toFloat" | "toStr" | "toBool" ), "()";
method_assign_bit_name = identifier, "[", literal_int, "]", "." ,identifier , "=", literal_str, ";";
method_named_bit_access = identifier, ".", identifier;
(* operacje bitowe *)
bmask_binary_operation = identifier, operator_bitwise, identifier, ";";
bmask_unary_negation = operator_negation, identifier, ";";
block = "{", { statement } "}";
(* instrukcje *)
statement = declaration_variable
| statement_if
| statement_while
| statement_return
| assign_or_call
| statement_break;
assign_or_call = access, ["=", expr ];
access = data, {'.', data};
data = identifier, [ "(", args, ")"], { "[", expression, "]" }
args = [ expression_list ]
statement_if = "if", "(", expression, ")", block,
{ "elif", "(", expression, ")", block },
[ "else", block ]
statement_while = "while", "(", expression ")", block;
statement_return = "return", [ expression ], ";";
statement_break = "break", ";";
(* wyrażenia *)
expression = expression_and, { operator_or, expression_and };
expression_and = expression_relation, { operator_and, expression_relation };
expression_relation = expression_additive, [ operator_relation, expression_additive ];
expression_additive = expression_multiplicative, { operator_additive, expression_multiplicative };
expression_multiplicative = expression_bitwise, { operator_multiplicative, expression_bitwise }
expression_bitwise = expression_unary, { operator_bitwise, expression_unary }
expression_unary = [ operator_negation ], expression_elementary;
expression_elementary = literal | "(", expression, ")" | access;
literal = literal_int | literal_float | literal_str | literal_bool | bit_list | literal_binary;
(* program *)
program = { declaration_function };
Użytkownik będzie informowany o błędzie w postaci:
ERROR OCURED AT [lineNumber : columnNumber ], [ ERROR_NAME ]
Poniżej znajduję się hierarchia błedów wraz z ich krytycznością.
Błąd krytyczny- niemożliwa jest dalsza interpretacja kodu i program się zatrzymujeBłąd niekrytyczny- może prowadzić do złych wyników, jednak nie przerywa dalszej analizy interpretera
Głównym atutem języka BitScope jest możliwość operacji na bardzo dużych maskach i submaskach bitowych, zatem przyjmując niskopoziomowe przeznaczenie języka wszelkie błędy powinny być wyłapywane.
Im niższy poziom abstrakcji tym większe mogą być skutki nieoczekiwanego działania programu, jeżeli w kodzie jest błąd.
Błędy na tym poziomie będą skupiały się na zapobiegnięciu sytuacjom, w którym niemożliwa jest poprawne rozpoznanie elementów języka, zbudowania tokenu:
| Typ Danych | Sytuacja | Typ Błędu | Krytyczność |
|---|---|---|---|
| - | - Symbol nie występujący w języku BitScope, dowolne znaki mogą wystąpić tylko w str lub w komentarzu po znaku # |
ErrorLexer |
Krytyczny |
int |
Przekroczenie zakresu wartości typu (określony też jako ilość znaków maksymalnej wartości) | ErrorLexer |
Krytyczny |
float |
Przekroczenie zakresu wartości typu (określony też jako ilość znaków maksymalnej wartości) | ErrorLexer |
Krytyczny |
bmask |
Przekroczenie zakresu wartości typu (określony też jako ilość znaków maksymalnej wartości) | ErrorLexer |
Krytyczny |
str |
Przekroczenie zakresu długości typu | ErrorLexer |
Krytyczny |
| - | Przekroczenie zakresu długości reprezentacji binarnej po znaku $ |
ErrorLexer |
Krytyczny |
all |
Przekroczenie zakresu długości identyfikatora | ErrorLexer |
Krytyczny |
| - | Użycie nieistniejacego w języku BitScope słowa, nierozpoznanego w składni jezyka (w tym użytego jako typ dannych) |
ErrorLexer |
Krytyczny |
| Sytuacja | opis | Typ Błędu | Krytyczność |
|---|---|---|---|
| Brak średnika po : - wywołaniu instrukcji return - wywołaniu funkcji - przypisaniu do zmiennej - deklaracji zmiennej |
MissingSemicolonError |
Krytyczny | |
Brak nawiasów { } (lub jednego z nich) w strukturach if, elif, else, while, funkcjach |
CriticalErrorParser |
Krytyczny | |
Brak nawiasów ( ) (lub jednego z nich) w strukturach if, elif, while, deklaracji funkcji |
CriticalErrorParser |
Krytyczny | |
Brak nawiasów [ ] (lub jednego z nich): - odnosząc się do konkrentego bitu w masce bitowej np. maskA[1.value - podając indexy bitów tworząc submaske np. maskA.submask([indexes], maskB) - podając indexy bitów przy metodzie maskA.append([indexes], SourceMaskObject) - podając indexy bitów przy metodzie np. maskA.reconfigure([indexes], SourceMaskObject) |
CriticalErrorParser |
Krytyczny | |
| Wywołanie funkcji lub metody z nadmiarowym przecinkiem podając argumenty np. funcA(b, c, d,); | CriticalErrorParser |
Krytyczny | |
| Zadeklarowanie zmiennej o nazwie już istniejącej w danym bloku (w tym submaski jako nowy obiekt używając nazwy identyfikatora już istniejącego w danym bloku) | CriticalErrorParser |
Krytyczny | |
| Użycie słowa kluczowego jako nazwa zmiennej | CriticalErrorParser |
Krytyczny | |
| Zadeklarowanie funkcji o już istniejącej nazwie | CriticalErrorParser |
Krytyczny | |
| Użycie niezadeklarowanej funkcji | CriticalErrorParser |
Krytyczny | |
| Użycie słowa kluczowego jako nazwa funkcji | CriticalErrorParser |
Krytyczny | |
| Błąd w nazwie zwracaego typu w definicji funkcji | CriticalErrorParser |
Krytyczny | |
| Brak określenia w definicji funkcji zwracanego przez nią typu danych | CriticalErrorParser |
Krytyczny | |
elif, else |
- Użycie elif bez wcześniejszego użycia if - Użycie else bez wcześniejszego użycia if i elif |
CriticalErrorParser |
Krytyczny |
if, elif |
- Brak warunku logicznego w konstrukcji if(expression) lub elif(expression) - Typ warunku inny niż logiczny np. if("Adam"), elif(15.23) |
CriticalErrorParser |
Krytyczny |
while |
- Brak warunku logicznego w konstrukcji while(expression) - Typ warunku inny niż logiczny np. while("Adam"), while(15.23) |
CriticalErrorParser |
Krytyczny |
return |
Brak słowa kluczowego return w funkcji o zadeklarowym zwracanym typie innym niż void |
CriticalErrorParser |
Krytyczny |
return |
Umieszczenie słowa kluczowego return w funkcji o typie zwracanym void |
CriticalErrorParser |
Krytyczny |
| Typ Danych | Sytuacja | Typ błedu | Krytyczność |
|---|---|---|---|
bmask |
Tworzenie maski poprzez mask(size), podając rozmiar innej wartości niż int |
InterpreterError |
Krytyczny |
| - | Wywołanie funkcji podając niepoprawną liczbę argumentów | InterpreterError |
Krytyczny |
| - | Wywołanie funkcji podając zły typ argumentu | InterpreterError |
Krytyczny |
bmask |
- Tworzenie submaski metodą .submask([indexes], SubmaskNameStr) podając nieistniejące indexy - Nadanie nazwy bitowi o indexie poza zakresem maski, lub wartości innej niż int np. maskA[1.5].name - Podanie indeksów poza zakresem maski nadrzędnej w metodzie .append([indexes], SourceMaskObject) - Podanie nieistniejącego indeksu maski nadrzędnej w metodzie .reconfigure([indexes], SourceMaskObject) |
InterpreterError |
Krytyczny |
bmask |
Nadanie bitowi danej maski już istniejącej nazwy innego bitu w tej masce. | InterpreterError |
Krytyczny |
bmask |
- Tworzenie submaski metodą .submask([bitName ...], SubmaskNameStr) podając nieistniejące nazwy bitów - Odwołanie się do bitu o nieistniejącej lub pustej nazwie |
InterpreterError |
Krytyczny |
bmask |
Usunięcie metodą .remove(SubmaskNameStr) submaski, która nie istnieje |
InterpreterError |
Niekrytyczny |
bmask |
Tworzenie submaski metodą mask([indexes, SourceMaskObject) podając nieistniejący obiekt maski nadrzędnej |
InterpreterError |
Krytyczny |
bmask |
Utworzenie metodą .submask([indexes], SubmaskNameStr) drugiej submaski o tej samej nazwie |
InterpreterError |
Krytyczny |
all |
Wywołanie nieobsługiwanej konwersji typu, np. str -> float lub konwersja bezpośrednio na typie float np. float 12.34.toInt() |
InterpreterError |
Krytyczny |
bool |
Wykonanie operacji arytmetycznej +, -, /, * na obiektach typu bool |
InterpreterError |
Krytyczny |
int, float, str, bool |
- Wykonanie operacji negacji poprzez inverse zmiennej typu innego niż bmask - Wykonanie operacji conjunction, disjunction, exclusive pomiędzy dwoma zmiennymi typu innego niż bmask |
InterpreterError |
Krytyczny |
int, float, str, bool |
Odwołanie się do wartości zmiennej, która została zadeklarowana ale nie została jej przypisana wartość | InterpreterError |
Krytyczny |
all |
Odwołanie się niezadeklarowanej zmiennej | InterpreterError |
Krytyczny |
all |
Użycie zmiennej poza blokiem, w którym została zadeklarowana | InterpreterError |
Krytyczny |
bmask |
Przypisanie do zmiennej typu bmask niezgodnej wartości z gramatyką |
AssigmentBmaskError |
Krytyczny |
int |
Przypisanie do zmiennej typu int niezgodnej wartości z gramatyką |
AssigmentRValueError |
Krytyczny |
float |
Przypisanie do zmiennej typu float niezgodnej wartości z gramatyką |
AssigmentRValueError |
Krytyczny |
bool |
Przypisanie do zmiennej typu bool niezgodnej wartości z gramatyką |
AssigmentRValueError |
Krytyczny |
str |
Przypisanie do zmiennej typu str niezgodnej wartości z gramatyką |
AssigmentRValueError |
Krytyczny |
| - | Funckja zwraca typ niezgodny z jej definicją | InterpreterError |
Krytyczny |
Aby uruchomić program należy przejść do katalogu src i wywołać program run.py podając jako pierwszy argument ścieżke do pliku .txt, po czym opcjonalnie paramtery wyołania głównej funkcji main. Program można wywołać używając flag -h --help lub -pt, określone poniżej:
>>>python3 run.py example1.txt 3 -h
usage: run.py [-h] [--pt] file [args ...]
Run BitScope, a general-purpose language with embedded bitmask operations.
positional arguments:
file (Required) Path to the code file.
args (0ptional) Arguments to pass to the program
options:
-h, --help show this help message and exit
--pt (0ptional)Print tree of nodes
Example executed: example1.txt:
int main(int a){
int b = a * 2;
return b;
}Result:
>>>python3 run.py example1.txt 3 --pt
6
Tree of code:
Program:
FunDef, [name: main, return type: TreeTypes.TYPE_INT, pos: (1, 1)]
VariableDeclarationStatement, [var_type: TreeTypes.TYPE_INT, var_name: a, pos: (1, 10)]
Block:
AssignmentStatement, [pos: (2, 5)
Left:
VariableDeclarationStatement, [var_type: TreeTypes.TYPE_INT, var_name: b, pos: (2, 5)]
Right:
MultiplyExpression, [pos: (2, 15)]
Left:
IdentifierExpression, [name: a, pos: (2, 13)
Right:
LiteralInt, [value: 2, pos: (2, 17)]
ReturnStatement, [pos: (3, 5)]
Expression:
IdentifierExpression, [name: b, pos: (3, 12)
Widoczność każej zmiennej jest w obszarze zamkniętym { }, w którym została zmienna zadeklarowana.
Ponadto zmienne można nadpisywać w zagnieżdżonych blokach:
void main(){
int a = 10;
if (1 < 3){
int a = 4; # nadpisanie zmiennej a wewnątrz bloku
print(a); # funkcja print wyświetli 4
}
print(a);
# funkcja print wyświetli 10, ponieważ blok w którym została zadeklarowana zmienna a z przypisaniem jej wartości 4 skończył się
}
Zatem interpreter będzie zarządzał granicami widoczności w taki sposób, aby odróżniać zmienne w blokach nadrzędnych od zmiennych w blokach podrzędnych.
Jednak dostęp do zmiennych z innych bloków będzie możliwy:
void main(){
int b = 10;
if (1 < 3){
int a = 4 + b; # zmienna a jest inicjalizowana z użyciem zmiennej b z nadrzędnego bloku
print(a); # funkcja print wyświetli 14
}
}
Aby były możliwe bardziej złożone wyrażenia korzystając z instrukcji warunkowych i pętli, interpreter będzie umożliwiał dostęp do zmiennych utworzonych w nadrzędnych blokach.
Gdy blok podrzędny się kończy, zmienna zadeklarowa w bloku podrzędnym nie będzie już widoczna w bloku nadrzędnym(przykład 2.10),
zasada ta jednak nie dotyczy submasek traktowanych jako atrybut obiektu maski bitowej:
void main(){
# tworzenie maski i submasek
bmask maskA = mask(8);
maskA.submask([1,2,3], "Gain");
if (1 < 3){
maskA.submask([4,5,6], "Gain2");
}
# po wykonaniu instrukcji if maskA ma dwie submaski
maskA.Gain = 4;
maskA.Gain2 = 5;
}
Projekt zostanie napisany w języku Python w wersji 3.11.
Przed lekserem zostanie dodana warstwa źródła tekstu czyli klasa Source.py przygotowująca kod do analizy leksykalnej, jej zadaniem jest:
- rozpoznaje koniec pliku poprzez sprawdzenie czy napotkany znak to
chr(3), czyliEOF(End of File) - ujdnolicenie znaków końca linii - w przypadku różnych systemów znak nowej linii jest inaczej oznaczany:
\ndla systemuUnix\r\ndla systemuWindows\rdlaClassic Mac OSWarstwa źródła tekstu normalizuje oznaczenia jakie napotka w zależności od systemu na jakim jest program uruchamiany do\n
Moduł leksera będzie odpowiadał za konwersję kodu źródłowego dostarczonego przez warstwę normalizacji na tokeny w sposób leniwy, co oznacza, że będzie analizował kolejny znak dopiero wtedy, gdy parser zgłosi zapotrzebowanie na nowy token.
Aby użytkownik otrzymywał status o błędzie w formacie przestawionym w punkcie 10, token musi zawierać:
Typ tokenu- jeden z wymienionych w sekcji 13.2.2, dobrany na podstawie napotkanej sekwencji znakówWartość tokenuNumer wiersza- początek sekwencji znaków budujących tokenNumer kolumny- początek sekwencji znaków budujących token
Aby możliwa była lokalizacja błędu poprzez numer wiersza i kolumny lekser będzie miał na początku zainicjalizowane dwa liczniki:
lineCounter- początkowa jego wartość to1, analizator przechodzi po kolejnych znakach natomiast gdy napotka znak\ninkrementuje liczniklineCountero1columnCounter- początkowa jego wartość to1, każdy kolejny znak, po którym przechodzi analizator inkrementuje licznikcolumnCountero1, z wyjątkiem znaku\n, który resetuje licznikcolumnCounterdo wartości początkowej1
Budując nowy token, zapisywany jest w obiekcie tokenu początek sekwencji znaków, czyli wartość dwóch liczników w chwili rozpoczęcia budowania nowego tokenu.
Tworzone typy tokenów:
| Typ | Tokeny |
|---|---|
| Tokeny typów danych | BMASK, BOOL, INT, FLOAT, STR |
| Token Void | VOID |
| Tokeny literału | LITERAL_BINARY, LITERAL_INT, LITERAL_FLOAT, LITERAL_STR, LITERAL_BOOL |
| Tokeny identyfikujące konstrukcje | DOT, COMMA, IF, ELIF, ELSE, WHILE, BREAK, RETURN |
| Tokeny operatorów binarnych | AND, OR, +, -, *, /, ==, !=, >=, >, <=, <, =, CONJUCTION, DISJUCTION, EXCLUSIVE, |
| Tokeny operatorów unarnych | NOT, INVERSE |
| Tokeny strukturalne | (, ), {, }, [, ], ; |
| Token identyfikatora | IDENTIFIER |
| Tokeny komentarza | # |
| Token końca pliku | EOF |
Jeżeli podczas analizy leksylanej analizator napotka symbol komentarza # zignoruje wtedy wszystkie znaki do końca linii (tzn. do znaku \n), tworząc token komentarza. Po przejściu do nowej linii rozpoczyna budowę nowego tokenu.
Jednak parser na wejsciu otrzymuje kolejne tokeny dostarczone przez klasę LexerNoComment używającą interfejsu ILexer, która odfiltrowuje tokeny komentarza, nie przekazując ich parserowi.
Po analizie leksykalnej, parser z otrzymanych od leksera odflitrowanych z komentarzy tokenów ma za zadanie zbudować drzewo składniowe,
Węzłami będą obiekty reprezentujące poszczególne instrukcje.
Parser przetwarza tokeny, które otrzymał od leksera i buduje z nich węzły:
| Węzeł | Konstrukcja | Dzieci węzła |
|---|---|---|
Node |
węzeł bazowy | wszystkie węzły |
FunDef |
deklaracaja funkcji | - |
Block |
blok kodu { ... } | - |
Program |
węzeł programu | - |
Statement |
węzeł bazowy dla instrukcji zdefiniowane w gramatyce | IfStatement, WhileStatement, ReturnStatement, VariableDeclarationStatement, |
BreakStetement, FunctionCallStatement, AssignmentStatement |
||
VariableDeclarationStatement |
węzeł deklaracji zmiennej | - |
IfStatement |
węzeł instrukcji warunkowej if | - |
BreakStetement |
węzeł instrukcji break | - |
AssignmentStatement |
węzeł instrukcji przypisania = | - |
WhileSetement |
węzeł pętli while | - |
FunctionCallStatement |
wywołanie funkcji | - |
ReturnStatement |
węzeł instrukji return expression; | - |
Expression |
węzeł bazowy dla wyrażeń zdefiniowane w gramatyce | LiteralExpression, BinaryExpression, UnaryExpression, AccessExpression |
BitList ,FunctionCallExpression, IdentifierExpression, IndexExpression |
||
LiteralExpression |
węzeł bazowy na literały | LiteralBool, LiteralInt, LiteralFloat,LiteralStr, |
LiteralBool |
węzeł przechowujacy literał bool | - |
LiteralInt |
węzeł przechowujacy literał int | - |
LiteralFloat |
węzeł przechowujacy literał float | - |
LiteralStr |
węzeł przechowujacy literał str | - |
BinaryExpression |
węzeł bazowy na wyrażenia binarne | AddExpression , ConjunctionExpression, DisjunctionExpression, NotEqualsExpression |
DivideExpression, EqualsExpression, ExclusiveExpression, GreatherEqualExpression |
||
GreatherExpression, LessEqualExpression, LessExpression, MultiplyExpression |
||
OrExpression , SubtractExpression |
||
AddExpression |
węzeł przechowujacy wyrażenia + | - |
AndExpression |
węzeł przechowujacy wyrażenia logiczne AND | - |
ConjunctionExpression |
węzeł przechowujacy wyrażenia conjunction (AND na masce bitowej) | - |
DisjunctionExpression |
węzeł przechowujacy wyrażenia disjunction (negacja maski bitowej) | - |
DivideExpression |
węzeł przechowujacy wyrażenia / dzielenia zmiennoprzecinkowego | - |
EqualsExpression |
węzeł przechowujacy wyrażenia == | - |
ExclusiveExpression |
węzeł przechowujacy wyrażenia excluisive (Xor na maskach bitowych) | - |
GreatherEqualExpression |
węzeł przechowujacy wyrażenia >= | - |
GreatherExpression |
węzeł przechowujacy wyrażenia > | - |
LessEqualExpression |
węzeł przechowujacy wyrażenia <= | - |
LessExpression |
węzeł przechowujacy wyrażenia < | - |
MultiplyExpression |
węzeł przechowujacy wyrażenia * | - |
NotEqualsExpression |
węzeł przechowujacy wyrażenia != | - |
OrExpression |
węzeł przechowujacy wyrażenia logiczne OR | - |
SubtractExpression |
węzeł przechowujacy wyrażenia odejmowania | - |
UnaryExpression |
węzeł bazowy na wyrażenia unarne | MinusExpression, InverseExpression ,NotExpression |
MinusExpression |
węzeł przechowujacy wyrażenia negacji poprzez - | - |
InverseExpression |
węzeł przechowujacy wyrażenia inwersji maski bitowej | - |
NotExpression |
węzeł przechowujacy wyrażenia negacji logicznej NOT | - |
AccessExpression |
węzeł przechowujacy wyrażenia dostępu poprzez operator . | - |
BitList |
węzeł przechowujacy wyrażenia listy bitów przekazanej jako parametr | - |
FunctionCallExpression |
węzeł przechowujacy wyrażenia wywołania funkcji | - |
IdentifierExpression |
węzeł przechowujacy wyrażenia identyfikatora | - |
IndexExpression |
węzeł przechowujacy wyrażenia indeksowane | - |
Kolejność budowania się drzewa z wezłów, wynika z gramatyki skonkretyzowanej, napisana gramatyka w punkcie 9 jest dla użytkownika, łatwiejsza do zrozumienia przy użyciu. |
Podczas budowania drzewa sprawdzane jest czy budowane z węzłów drzewo są dozwolone, wszystkie błedy na etapie parsera znajdują się w punkcie 10.2.
Dochodzi więc na tym etapie do analizy semantycznej, w której już mogą zostać wykryte niektóre błędy. Przykładowo nigdy nie powstanie węzeł AccessStatement który ma prawe dziecko będące np literałem int, ponieważ parser zbuduje ten węzeł tylko jeżeli prawe dziecko to expression lub function call
Zadania interpretera:
- Działanie interpretera bazuje na użyciu wzorca projektowego
Wizytator - Interpreter ma za zadanie przejść po drzewie skłądniowym stworzonym przez parser, wykonując każdy węzeł znajdujący się w drzewie i sprawdzając jego semantykę
- Interpreter zostaje zainicjalizowany ze środowiskiem
env, posiadającym stos kontekstów funkcji (instancji klasyFunctionBlock) - Interpreter wchodząc w nową funkcje dodaje do stosu kontekstów funkcji w
envnowyFunctionContext - Wchodząc w nowy blok, występujący w:
if,elif,else,while,nowym ciele funkcjiinterpreter tworzy nowyBlockContextdodajac go do aktualnegoFunctionBlock, a po zakończeniu bloku przywraca poprzedni kontekst (bloku nadrzędnego) zdejmującBlockContextze stosu bloków znajdującym się w instancji klasyFunctionBlock - Obsługuje stos wywołań funkcji w zależności od kontekstu, podanych parametrów i już istniejących zmiennych
- Identyfikuje i zgłasza użytkownikowi błędy semantyczne określone w punkcie
10.3
Do testowania kolejnych etapów projektu zostaną wykorzystane testy jednostkowe z użyciem pytest:
Sposób testowania:
- porównanie czy z danego tekstu wygenerują się oczekiwane tokeny
- sprawdzenie normalizowania białych znaków czy powielone białe znaki zostaną zmniejszone do jednego znaku
- sprawdzenie jak białe znaki wpływają na budowanie się tokenów, tzn. czy biały znak nie zaburza budowania się tokenu
- sprawdzenie zamiany znaków końca linii na znormalizowany znak
\n- sprawdzenie generowania błędów przez lekser przy niezdefiniowanych symbolach lub słowach - sprawdzenie sytuacji przekroczenia długości literałów lub np. niezamkniętego
strkończącego program - sprawdzenie jak lekser reaguje na komentarze, czy pomija je nieprzekazując ich dalej do parsera
- sprawdzenie czy wygenruje się znak końca pliku
EOF
Sposób testowania:
- sprawdzenie na danym zestawie tokenów, jaką kolejność wykonywania/budowania w drzewie przypisał im parser, następnie zostanie to porównane ze wzorcową kolejnością budowania
- sprawdzenie na błędnym zestawie tokenów czy parser poprawnie generuje błędy np. użycie niezadeklarowanej zmiennej, gdy budując drzewo nie znajdzie jej deklaracji
- sprawdzenie czy węzły poprawnie się generują tzn. czy dany węzeł ma oczekiwane dzieci określone w punkcie
13.3.1
Sposób testowania:
- sprawdzenie logiki działania metod na obiekcie
bmask - sprawdzenie logiki wywołań funkcji, instrukcji przypisania, konwersji typu, zgodności typów danych
- sprawdzenie działania logiki instrukcji warunkowych i pętli
- sprawdzenie zakresu widoczności i zarządzania kontekstem zmiennych w ramach bloku
{ }