Język służy do operacji na tablicach. Głównymi inspiracjami były języki
- Python - list comperhension
- Rust - wszystko jest wyrażeniem
- Silnie typowany
- Dynamicznie typowany
- Zmienne są mutowalne
- Jednowątkowy, synchroniczny
- Zmienne widoczne tylko w blokach kodu i ich zagnieżdżeniach
- Prawie wszystko jest wyrażeniem
Jednoliniowe // ...
i wieloliniowe /* ... */
Zawierać będzie 3 typy podstawowe: bool, int, float
oraz typ listowy: [].
Dostępny będzie też typ string zachowujący się jak tablica znaków.
W implementacji zaistnieją też typy none.
Typ none posłuży do realizacji typów wyrażeń,
których nie można przypisać do zmiennych.
Wszystko oprócz definicji funkcji jest wyrażeniem.
Znak ; zamienia wyrażenia na instrukcje.
Określone są w nawiasach klamrowych { }
i zawiera serię instrukcji
opcjonalnie zakończonych wyrażeniem.
Bloki kodu przyjmują wartość ostatniego wyrażenia.
{
bar();
10
}
ma typ int i wartość 10 oraz może być przypisany do zmiennej.
W przypadku braku wyrażenia
{
bar();
}
ma typ none i nie ma on wartości, czyli nie da się go przypisać do zmiennej.
Dla bool są to true lub false.
true
false
Dla int jest to zero, lub ciąg cyfr, który nie zaczyna się zerem.
123
0123 // niepoprawne
Dla float jest to int po którym występuje . oraz ciąg dowolnych cyfr (conajmniej jedna).
0.1
1.0
0. // niepoprawne
Dla string jest to tekst zawarty w cytaty.
Może być wielo-linijkowy.
Możliwa jest ucieczka za pomocą znaku \.
"Hello world!"
"Hello
world!"
"Escape quotation\""
"Escape the escape\\"
Zaczyna się od słowa let,
następnie podana jest nazwa zmiennej oraz jej typ,
oddzielony operatorem :.
Zmienna zawsze musi zostać zainicjowana, więc od razu następuje przypisanie wartości.
let x: int = 10;
Deklaracja jest wyrażeniem, które zwraca wartość prawej strony, czyli da się wykonać kilka deklaracji w następujący sposób:
let y: int = let x: int = 10
Zaczyna się od nazwy zmiennej,
a następnie operatora przypisania = i wartość odpowiedniego typu.
Przypisanie jest wyrażeniem, które przyjmuje wartość prawej strony przypisania.
x = -10
ma wartość -10.
let y: int = x = 10;
to poprawne wyrażenie.
Przykład przypisania stałej do typu listowego
xs = [1, 2, 3, 4, 7];
Dla listy string
let str: string = "Hello world!";
let str: string = "Hello
world!";
Zawiera słowo kluczowe if,
predykat typu bool,
następnie blok kodu.
Opcjonalnie po bloku kodu można użyć słowa kluczowego else
oraz kolejnego bloku kodu.
W przypadku pary if else przyjmuje wartość bloku kodu gałęzi, która została wykonany.
let x: int = if y < 3 {
1
} else {
20
};
W przypadku samego if jest podobnie, wyrażenie przyjmie tutaj typ none, bo blok kodu kończy się ;.
if y < 5 {
foo();
};
-
int,float
Operator-zwraca oryginalny typ -
bool
Operator!zwraca typbool
-
int
Operatory+,-,*,/,%zwracają typint.
Operatory==,<,<=,>,>=zwracają typbool. -
float
Operatory+,-,*,/zwracają typfloat.
Operatory==,<,<=,>,>=zwracają typbool. -
bool
Operatory==,!=,|,&zwracają typbool
-
[]
Wykonują operacje na elementach list.
W przypadku operatorów binarnych wynikowa lista ma długośc większej listy wejściowej, a elementy bez pary nie zmieniają wartości.
Operator[i]zwraca pojedynczy element pod indeksema(który jest wyrażeniem typuint). -
stringOperator[i], który zwrócistringz pojedynczym znakiem z danego indeksu.
Operator+, który zwraca konkatenacje łańcuchów wejściowych.
Operator==,!=, który zwracabool
Operator [a..b], który zwraca listę o elementach od indeksu a do indeksu b (które są wyrażeniami typu int).
Operacje mogą być zawarte w nawiasach ( ), aby wymusić inny priorytet wykonania.
Dostępne są 2 typy.
Pętla 'dopóki' zaczyna się od słowa kluczowego while,
następnie podany jest wyrażenie o typie bool,
a na koniec blok kodu.
Pętla 'dla' zaczyna się od słowa kluczowego for,
następnie podana jest nazwa zmiennej,
która przyjmie wartości kolejnych elementów listy,
po niej wystąpi słowo kluczowe in
oraz wyrażenie o typie listy (ale nie string, bo nie posiada typu pojedynczego),
na końcu jest blok kodu.
Pętle zwracają listę o ile typ zwrotny bloku jest typem bazowym: int, float, bool.
let a: int = 5;
let b: int = 10;
let a_b_range: [] = while a < b {
a = a + 1;
a
};
let xs: [] =
let incremented_xs: [] = for x in xs {
x + 1
};
Mają postać identyfikatora, a następnie ( ), w których zawarte są argumenty.
Naturalnie przyjmują wartość obliczoną z wywołania funkcji.
Jeżeli funkcja nie definiowała typu zwrotnego, to zwraca typ none.
fn foo() -> int {
10
}
fn main() {
let x: int = foo();
}
fn bar() {
foo();
}
fn main() {
let x: ??? = bar(); // Nie można przypisać, bo typ `none`
}
Słowo kluczowe return jest wyrażeniem, które zawsze zwraca none.
Więcej o nim później.
Zaczyna się od słowa kluczowego fn,
a następnie nazwy funkcji.
Potem w nawiasach ( ) podane są parametry oddzielone przecinkiem.
Każdy parametr to jego nazwa oraz typ przedzielone :.
Po parametrach możliwe jest dodanie typu zwrotnego po operatorze ->.
Na końcu podane jest ciało funkcji w postaci bloku kodu.
Przykładowe definicje funkcji:
fn negative_together(
x: int,
y: int
) -> bool {
let z: int = x + y;
z < 0
}
fn do_nothing() {}
fn print_but_dont_return(x: int) {
print(x);
}
Może istnieć tylko jedna funkcja o danej nazwie, wyjątkami są funkcje wbudowane:
print(0)
print(1.0)
print(true)
print("Hello world!", "Hello again!")
Funkcja zwróci wartość wyrażenia bloku kodu ciała funkcji,
ale możliwe jest wcześniejsze zwrócenie wartości x
poprzez instrukcję return x.
W przypadku funkcji, które nie zwracają wartości (zwracają typ none)
można zastosować samo słowo kluczowe return.
Nawiasy ( ) mogą wymusić inną kolejnośc.
Priorytet Operator/-y Opis -arność Łączność Pozycja
9 (a, b, ...) wywołanie funkcji N-nary - suffix
[a..b] dostęp do pod-listy Trynary - suffix
[a] dostęp do indeksu Binary - suffix
8 - negacja arytmetyczna Unarny - prefix
! negacja logiczna Unarny - prefix
7 * mnożenie Binarny lewostronna -
/ dzielenie Binarny lewostronna -
% reszta z dzielenia Binarny lewostronna -
6 + dodawanie Binarny lewostronna -
- odejmowanie Binarny lewostronna -
5 == równość Binarny lewostronna -
!= nierówność Binarny lewostronna -
< mniejszość Binarny lewostronna -
<= mniejszość lub równość Binarny lewostronna -
> większość Binarny lewostronna -
>= większośc lub równość Binarny lewostronna -
4 & koniunkcja logiczna Binarny lewostronna -
3 | alternatywa logiczna Binarny lewostronna -
2 = przypisanie wartości Binarny prawostronna -
1 return wyjście z funkcji Unarny - prefix
let deklaracja zmiennej Binarny prawostronna -
Zaoferuje metody:
print(arg1)
wypisze wartość argumentu do strumienia wyjściowegocast_int(arg1),cast_float(arg1),cast_bool(arg1),cast_string(arg1)
spróbuje zamienić wartość jednego typu na drugipush(arg1, arg2)
doda element do końca listy i ją zwróci (poprawne tylko dla arg1 typu[])length(arg1)
zwróci długość (poprawne tylko dla argumentów typów[],string)
Język jako pierwszą wywoła funkcję main,
która nie przyjmuje żadnych argumentów
i nie zwraca żadnej wartości.
Wykorzystam język Rust z paczką utf8-chars,
która pozwala na czytanie pojedynczych znaków utf-8 z bufora,
w celu realizacji skanera.
Projekt będzie podzielony na kilka modułów,
każdy będący w stanie działać niezależnie,
co ułatwi tworzenie testów.
Moduły, które na pewno się znajdą to:
- analizator leksykalny
- analizator składniowy
- interpreter
Język pozwoli na interpretacje pliku lub strumienia wejściowego w formacie utf-8.
Obsługa wielu plików może być łatwo zaimplementowana,
ale taka naiwna implementacja pogorszyłaby czytelność języka.
(funkcja main tylko w jednym pliku,
nie wiadomo gdzie zdefiniowane zostały funkcje)
Język będzie uruchamiany z wiersza poleceń.
Uruchomienie bez flag wyświetli informacje pomocnicze.
Uruchomienie z flagą -f <path> wykorzysta podany plik jako wejście.
Uruchomienie z flagą -i wykorzysta strumień wejściowy procesu.
Błędy będą ignorowane w czasie analizy (leksykalnej, składniowej), ale ich wystąpienie uniemożliwi wykonanie programu. Wiadomości o wszystkich błędach zostaną wypisane do strumienia błędów.
Wystąpienie błędu w czasie wykonania kodu, np.:
- dzielenie przez 0
- błąd zamiany typu (np.
stringwfloat) - dostęp do nieistniejącego indeksu, zmiennej
spowoduje zatrzymanie wykonania i zwrócenie komunikatu o błędzie do strumienia błędów.
Automatyczne z wykorzystaniem wbudowanej architektury języka Rust, cargo test.
Testowanie działania poprawnego i obsługi błędów.
Testy jednostkowe:
- wykrywanie pojedynczych tokenów
- wykrywanie fragmentu składni
- wywoływanie pojedynczych tokenów
Testy integracyjne:
- wykrywanie serii tokenów
- wykrywanie całego drzewa składniowego
Testy akceptacyjne:
- wykonanie przykładowych fragmentów kodu