Język Latte jest językiem imperatywnym, niemal podzbiorem imperatywnego fragmentu języka Java i może być z łatwością nań przetłumaczony.
Niniejszy opis jest umyślnie nieprecyzyjny i oparty na przykładach; dospecyfikowanie języka jest częścią zadania. Dla ułatwienia udostępniamy gramatykę języka bazowego w formacie dla BNFC (dla C++ lepiej użyć innej wersji).
Archiwum z programami testowymi:
lattests121017.tgz z 17.10.2012, MD5:49ca5702ca9795fb8f33d52b0b3c6fc3
// Hello world
int main () {
printString("hello world") ;
return 0 ;
}
// wypisz liczby parzyste do 10
int main () {
int i ;
i = 0 ;
while (i < 10){
if (i % 2 == 0) printInt(i) ;
i++ ;
}
printInt(i) ;
return 0 ;
}
int main () {
printInt(fact(7)) ;
printInt(factr(7)) ;
return 0 ;
}
// iteracyjnie
int fact (int n) {
int i,r ;
i = 1 ;
r = 1 ;
while (i < n+1) {
r = r * i ;
i++ ;
}
return r ;
}
// rekurencyjnie
int factr (int n) {
if (n < 2)
return 1 ;
else
return (n * factr(n-1)) ;
}
Program w języku Latte jest listą definicji funkcji. Na definicję funkcji składa się typ zwracanej wartości, nazwa, lista argumentów oraz ciało. Funkcje muszą mieć unikalne nazwy. W programie musi wystąpić funkcja o nazwie main zwracająca int i nie przyjmująca argumentów (od niej zaczyna się wykonanie programu). Funkcje o typie wyniku innym niż void muszą zwracać wartość za pomocą instrukcji return
Funkcje mogą być wzajemnie rekurencyjne; co za tym idzie mogą być definiowane w dowolnej kolejności (użycie funkcji może występować przed jej definicją)
Program. Program ::= [TopDef] ; FnDef. TopDef ::= Type Ident "(" [Arg] ")" Block ; separator nonempty TopDef "" ; Arg. Arg ::= Type Ident; separator Arg "," ;
Deklaracje zmiennych mogą występować w dowolnym miejscu bloku, jednak każda zmienna musi być zadeklarowana przed użyciem. Jeśli zmienna nie jest jawnie inicjalizowana w momencie deklaracji, jest inicjalizowana wartością domyślną (0 dla int, "" dla string, false dla bool).
Zmienne zadeklarowane w bloku nie są widoczne poza nim i przesłaniają zmienne o tej samej nazwie spoza bloku. W obrębie bloku zmienne muszą mieć unikalne nazwy.
Block. Block ::= "{" [Stmt] "}" ; separator Stmt "" ; Empty. Stmt ::= ";" ; BStmt. Stmt ::= Block ; Decl. Stmt ::= Type [Item] ";" ; NoInit. Item ::= Ident ; Init. Item ::= Ident "=" Expr ; separator nonempty Item "," ; Ass. Stmt ::= Ident "=" Expr ";" ; Incr. Stmt ::= Ident "++" ";" ; Decr. Stmt ::= Ident "--" ";" ; Ret. Stmt ::= "return" Expr ";" ; VRet. Stmt ::= "return" ";" ; Cond. Stmt ::= "if" "(" Expr ")" Stmt ; CondElse. Stmt ::= "if" "(" Expr ")" Stmt "else" Stmt ; While. Stmt ::= "while" "(" Expr ")" Stmt ; SExp. Stmt ::= Expr ";" ;
Dla potrzeb analizy semantycznej może być pożyteczne wprowadzenie typów funkcyjnych.
Int. Type ::= "int" ; Str. Type ::= "string" ; Bool. Type ::= "boolean" ; Void. Type ::= "void" ; internal Fun. Type ::= Type "(" [Type] ")" ; separator Type "," ;
EVar. Expr6 ::= Ident ; ELitInt. Expr6 ::= Integer ; ELitTrue. Expr6 ::= "true" ; ELitFalse. Expr6 ::= "false" ; EApp. Expr6 ::= Ident "(" [Expr] ")" ; EString. Expr6 ::= String ; Neg. Expr5 ::= "-" Expr6 ; Not. Expr5 ::= "!" Expr6 ; EMul. Expr4 ::= Expr4 MulOp Expr5 ; EAdd. Expr3 ::= Expr3 AddOp Expr4 ; ERel. Expr2 ::= Expr2 RelOp Expr3 ; EAnd. Expr1 ::= Expr2 "&&" Expr1 ; EOr. Expr ::= Expr1 "||" Expr ;
Wyrażenie logiczne zwracają typ boolean i są obliczane leniwie (drugi argument nie jest wyliczany gdy pierwszy determinuje wartość wyrażenia).
Napisy moga występować jako: literały, wartości zmiennych, argumentów i wyników funkcji
Napisy mogą być użyte jako argumenty wbudowanej funkcji printString
Napisy mogą być konkatenowane przy pomocy operatora +. Wynikiem tej operacji jest nowy napis będący konkatenacją argumentów
void printInt(int)
void printString(string)
void error()
int readInt()
string readString()
Funkcja error wypisuje runtime error i kończy wykonywanie programu.
Funkcja readString wczytuje jedną linię z wejścia i daje ją jako wynik.
Przekazanie napisu jako parametru odbywa sie poprzez przekazanie przez wartość referencji do napisu.
Programy w Latte mogą być bez większych trudności przetłumaczone na język Java, poprzez opakowanie wszystkich funkcji w klasę jako publiczne statyczne metody. Metoda main będzie musiała dostać inną sygnaturę. Pewną trudność mogą sprawić zagnieżdżone bloki. Niektóre rozszerzenia mogą oczywiście wymagać dodatkowych zabiegów.
Tablice jak w Javie, czyli zmienne typu tablicowego zawierają referencję do tablicy, zaalokowanej na stercie. Tablice są tworzone jawnie, przy użyciu operatora new. Zmienne typu tablicowego mają atrybut length (np. a.length)
Przykłady deklaracji tablic
int[] a;
string[] b;
Tworzenie tablic może (ale nie musi) być połączone z deklaracją:
a = new int[20];
int[] c = new int[30];
Funkcje mogą przyjmować tablice jako argumenty i dawać je w wyniku:
int[] sum (int[] a, int[] b) {
int[] res = new int [a.length];
int i = 0;
while (i < a.length) {
res[i] = a[i] + b[i];
i++;
}
return res;
}
W ramach tego rozszerzenia należy również zaimplementować prosty wariant pętli foreach:
for (int x : a)
printInt(x);
Testy dla tego rozszerzenia znajdują się w katalogu extensions/arrays1.
Rudymentarna wersja obiektów, uwzględniająca jedynie atrybuty, bez metod i dziedziczenia. Zmienne typu strukturalnego zawierają referencję do struktury zaalokowanej na stercie. Przypisanie oznacza przypisanie referencji. Przekazanie struktury jako parametru odbywa sie poprzez przekazanie przez wartość referencji. Powyższe odnosi sie równiez do następnego rozszerzenia ("Obiekty")
class list {
int elem;
list next;
}
int main() {
printInt(length(fromTo(1,50)));
printInt(length2(fromTo(1,100)));
}
int head (list xs) {
return xs . elem;
}
list cons (int x, list xs) {
list n;
n = new list;
n.elem = x;
n.next = xs;
return n;
}
int length (list xs) {
if (xs==(list)null)
return 0;
else
return 1 + length (xs.next);
}
list fromTo (int m, int n) {
if (m>n)
return (list)null;
else
return cons (m,fromTo (m+1,n));
}
int length2 (list xs) {
int res = 0;
while (xs != (list)null) {
res++;
xs = xs.next;
}
return res;
}
Testy dla tego rozszerzenia znajdują się w katalogu extensions/struct.
Przykład klasy:
int main () {
Counter c;
c = new Counter;
c.incr();
c.incr();
c.incr();
int x = c.value();
printInt(x);
return 0;
}
class Counter {
int val;
void incr () {val++; return;}
int value () {return val;}
}
Przykład dziedziczenia:
class Point2 {
int x;
int y;
void move (int dx, int dy) {
x = x + dx;
y = y + dy;
}
int getX () { return x; }
int getY () { return y; }
}
class Point3 extends Point2 {
int z;
void moveZ (int dz) {
z = z + dz;
}
int getZ () { return z; }
}
class Point4 extends Point3 {
int w;
void moveW (int dw) {
w = w + dw;
}
int getW () { return w; }
}
int main () {
Point2 p = new Point3;
Point3 q = new Point3;
Point4 r = new Point4;
q.move(2,4);
q.moveZ(7);
p = q;
p.move(3,5);
r.move(1,3);
r.moveZ(6);
r.moveW(2);
printInt(p.getX());
printInt(p.getY());
printInt(q.getZ());
printInt(r.getW());
return 0;
}
Testy dla tego rozszerzenia znajdują się w katalogu extensions/objects1.
Przykład:
class Node {
Shape elem;
Node next;
void setElem(Shape c) { elem = c; }
void setNext(Node n) { next = n; }
Shape getElem() { return elem; }
Node getNext() { return next; }
}
class Stack {
Node head;
void push(Shape c) {
Node newHead = new Node;
newHead.setElem(c);
newHead.setNext(head);
head = newHead;
}
boolean isEmpty() {
return head==(Node)null;
}
Shape top() {
return head.getElem();
}
void pop() {
head = head.getNext();
}
}
class Shape {
void tell () {
printString("I'm a shape");
}
void tellAgain() {
printString("I'm just a shape");
}
}
class Rectangle extends Shape {
void tellAgain() {
printString("I'm really a rectangle");
}
}
class Circle extends Shape {
void tellAgain() {
printString("I'm really a circle");
}
}
class Square extends Rectangle {
void tellAgain() {
printString("I'm really a square");
}
}
int main() {
Stack stk = new Stack;
Shape s = new Shape;
stk.push(s);
s = new Rectangle;
stk.push(s);
s = new Square;
stk.push(s);
s = new Circle;
stk.push(s);
while (!stk.isEmpty()) {
s = stk.top();
s.tell();
s.tellAgain();
stk.pop();
}
return 0;
}