ELI jest zestawem narzędzi służących do uproszczonej budowy programów przetwarzających tekst na podstawie specyfikacji problemu przy jak najmniejszym nakładzie sił poświęconych na kwestie implementacyjna. Łączy on funkcjonalność programów takich jak lex i yacc, dodając jednak mechanizmy pozwalające w wygodny sposób pisać mechanizmy implementujące właściwe działanie np. kompilatora, które to fragmenty w yacc musiały być pisane w większości ręcznie
ELI nie jest narzędziem ograniczonym tylko do generowania translatorów - może być stosowany w dowolnej sytuacji wymagającej przetwarzania tekstu, w której zasady owego przetwarzania się często zmieniają, a jednocześnie wymagana jest wysoka wydajność
W dowolnym programie typu ELI proces przetwarzania tekstu można podzielić na kilka faz. Zawsze występują co najmniej trzy z nich:
W typowym modelu konstrukcji translatorów za pierwszą z tych faz odpowiada analizator generowany przez lex, za drugą - parser generowany przez yacc, natomiast trzecia faza odpowiada fragmentom kodu w C, o które programista musi rozbudować specyfikację gramatyki opisaną przez plik wejściowy yacc.
Co gorsza, te trzy etapy są w takiej sytuacji zupełnie oddzielone - nie ma możliwości wykorzystania informacji dostępnej w trakcie rozpoznawania słów do analizy gramatycznej. ELI rozwiązuje ten problem wykonując wszystkie fazy w ramach jednego programu. Co więcej fazę analizy gramatycznej rozpatruje się z regułu przed analizą leksykalną!
W rzeczywistości ELI traktuje proces przetwarzania tekstu jako bardziej złożony proces, dzieląc go na więcej części, z których nie wszystkie są obowiązkowe. Jako przykład można podać tzw. overload resolution - wybór właściwej wersji operatora przeciążonego. W ogólnym przypadku ten etap nie występuje, jest on specyficzny dla programów przetwarzających języki programowania (i to dość konkretne)
Przykład:
Position: Latitude Longtitude . Latitude: NS Coordinate . Longitude: EW Coordinate . NS: 'N' / 'S' . EW: 'E' / 'W' . Coordinate: Integer Float .
Podana gramatyka ma 8 symboli: Position, Latitude, Longitude, NS, EW,
Coordinate, Integer, Float, cztery literały: 'N', 'S', 'E', 'W'
i osiem reguł (x: y / z . to skrót dla dwóch reguł postaci
x: y . i x: z .).
Symbole, które nie są zdefiniowane przez gramatykę nazywamy końcowymi (terminal symbols), natomiast symbole zdefiniowane przez reguły to symbole pośrednie (nonterminal symbols).
Symbole które są "najwyższego poziomu" (nie ma do nich odwołań w innych definicjach) nazywamy axiomami (axiom).
Pod tym pojęciem w ELI rozumiany jest proces przetwarzania tekstu i usunięcia z niego wszystkich nieznaczących (non-significant) symboli pozostawiając pozostałe fragmenty w niezmienionej postaci. Symbol definiujemy jako znaczący jeżeli jest w odpowiedniej gramatyce symbolem końcowym albo literałem.
W tym miejscu widać przewagę ELI nad kombinacją lex+yacc. Informacja leksykalna o literałach jest bowiem uzyskiwana bezpośrednio z gramatyki! Użytkownik musi podać wzorce tylko dla symboli końcowych. Ich postać jest typowa i opiera się o wyrażenia regularne. Oto przykład:
Integer: $[0-9]+ Float: $[0-9]+"."[0-9]+
(Są to mocno uproszczone definicje liczb na potrzeby powyższej gramatyki)
Opis gramatyki w połączeniu z opisem symboli końcowych pozwala nam na zbudowanie drzewa odpowiadającego dowolnemu pojęciu zgodnego z gramatyką (typowo wykonujemy to dla axiomów - odpowiadają one programom w danym języku, lub specyfikacji problemu w innej formie). Jednak samo takie drzewo nic nam nie daje - z reguły chcemy dodać w tym momencie jakieś akcje które mają zostać wykonane, jeżeli program wynikowy w trakcie analizy dojdzie do danego miejsca w drzewie. Oto przykład takiej definicji w składni ELI:
SYMBOL set_body COMPUTE
SYNT.Size=CONSTITUENTS set_element.Sym WITH (int, ADD, ARGTOONE, ZERO);
END;
Opisuje ona akcję, jaka ma zostać wykonana, jeżeli w trakcie analizy napotkany
zostanie węzeł typu set_body - konstrukcja ta będzie dokładniej wyjaśniona
później.
Ostatnią fazą przetwarzania jest zamiana drzewa i dodatkowych informacji zebranych w poprzednim etapie na plik wyjściowy o żądanej składni. W ELI wykorzystywany jest mechanizm wzorców (templates), którego działanie polega na tym, że tworzymy szablony w których wszystkie elementy zmienne zastępujemy specjalnymi symbolami. Oto przykład:
StringList:
"char **array_named_"$1"[] = {\n"
$2/* Ciąg łańcuchów znaków oddzielonych przecinkami*/ "};\n\n"
Zdefiniowano wzorzec elementu StringList, który jest parametryzowany
przez: nazwę tablicy ($1) oraz jej zawartość ($2). ELI
dostarcza mechanizmy, które
pozwalają "wypełnić" taki wzorzec obiektami uzyskanymi np. przy przetwarzaniu drzewa.
Jak widać ELI operuje na kilku formatach plików - przyjęto ich rozróżnianie na podstawie rozszerzenia nazwy. Oto kilka ważniejszych przykładów:
.specs: zbiorcza specyfikacja problemu (zawiera nazwy plików
które składają się na problem oraz dyrektywy włączające
opcjonalne moduły - patrz niżej)
.con: definicja gramatyki
.gla: analizator leksykalny
.lido: operacje dokonywane na węzłach drzewa (semantyka)
.ptg: szablony do generowania tekstu wynikowego
.clp: dane wejściowe dla modułu Command Line Processing
(patrz p.6.2.)
.fw: meta-plik, zawierający kilka innych plików zapisanych w specjalnym
formacie, oraz dokumentację problemu (patrz p.7.)
.phi: plik przeznaczony do wstawienia w kod wynikowy
.eta: plik przeznaczony do wstawienia w kod źródłowy specyfikacji
Po uruchomieniu systemu ELI otrzymujemy prompt postaci ->.
Aby rozpocząć pracę należy zatem znać co najmniej dwie podstawowekomendy:
sets.specs :exesets.specs :sourceMakefile kompilujący ten kod do postaci wykonywalnej
Zdefiniowany jest następujący problem: przetworzyć ciąg list następującej postaci:
colors{red blue green}
bugs{ant spider fly moth bee}
verbs{crawl walk run fly}
na odpowiadający jej program w języku C o postaci:
int number_of_sets=3;
char *name_of_set[] = {
"colors",
"bugs",
"verbs"};
int size_of_set[] = {
3,
5,
4};
char *set_of_colors[] = {
"red",
"blue",
"green"};
char *set_of_bugs[] = {
"ant",
"spider",
"fly",
"moth",
"bee"};
char *set_of_verbs[] = {
"crawl",
"walk",
"run",
"fly"};
char **values_of_set[] = {
set_of_colors,
set_of_bugs,
set_of_verbs};
dbając o to, żeby w żadnym z sets nie powtórzyła się dwukrotnie ta sama wartość.
Cztery niżej opisane pliki są ujęte we wspólnym pliku sets.spec,
który zawiera ich nazwy oraz biblioteki, które mają zostać włączone w procesie
kompilacji.
sets.con)Jak już wcześniej wspomniano, podaje się tutaj zależności logiczne, konstrukcję gramatyki, a nie np. konstrukcję słowa, chociaż elastyczność języka pozwala również i na to. Oto zawartość pliku:
text: set_defs .
set_defs: set_def / set_defs set_def .
set_def: set_name '{' set_body '}' .
set_name: word .
set_body: elements .
elements: set_element / elements set_element .
set_element: word .
Zasada konstrukcji reguł jest taka sama jak w poprzednim przykładzie (p.2.2.1.)
word.gla)Oprócz zdefiniowania "słowa", które jest jedynym symbolem terminalnym gramatyki, wstawiamy dodatkowo dyrektywę wbudowaną, która powoduje, że ignorowane są komentarze w stylu języka C:
word: $[a-zA-Z]+ [mkidn]
C_COMMENT
symbol.lido)Na tym etapie mamy już formalnie zdefiniowaną gramatykę. Możemy jednak dodawać nowe akcje do wykonania dla dowolnego elementu języka (czyli tak naprawdę dla dowolnego węzła drzewa).
Jak pamiętamy, na język narzucone jest wymaganie, aby elementy na listach nie
powtarzały się. W Eli zawarty jest gotowy moduł potrafiący sprawdzać tego typu
warunki. Aby go włączyć musimy do pliku .spec dołączyć następujące linie
$/Name/AlgScope.gnrc :inst $/Prop/Unique.gnrc :inst
(Pierwszy z tych modułów jest potrzebny po to, aby określić zakres w jakim sprawdzamy jednoznaczność).
ATTR Sym: int;
SYMBOL Entity INHERITS IdDefScope, Unique COMPUTE
SYNT.Sym=TERM;
IF(NOT(THIS.Unique),
message(ERROR,"Multiply-defined word", 0, COORDREF));
END;
SYMBOL text INHERITS RootScope, RangeUnique END;
SYMBOL set_body INHERITS RangeScope END;
SYMBOL set_name INHERITS Entity END;
SYMBOL set_element INHERITS Entity END;
code.fw)Przy ostatnim etapie przetwarzania obowiązuje (opisany w p.2.2.4.) rozdział
"wyglądu" kodu wynikowego od jego "zawartości". Aby używać opisanych tam narzędzi
należy w pliku .specs załadować moduł LeafPtg:
$/Output/LeafPtg.gnrs :inst
Obydwie te części są jednak zawarte w tym samym pliku. Może on również zawierać informacje dodatkowe, takie jak np. tekstowy opis specyfikacji - patrz p.7.).
@O@<code.ptg@>@{
...
@}
@O@<code.lido@>@{
...
@}
code.ptg)
Table:
"int number_of_sets = " $/*integer*/ ";\n\n"
"char *name_of_set[] = {\n"
$/*list of set names*/ "};\n\n"
"int size_of_set[] = {\n"
$/*list of set sizes*/ "};\n\n"
$/*list of sets*/
"char **values_of_set[] = {\n"
$/*list of set representations*/ "};\n"
Set:
"char *set_of_" $/*set name*/ "[] = {\n"
$/*list of set elements*/ "};\n\n"
List:
$ ",\n" $
Quoted:
"\"" $ "\""
Name:
"set_of_" $
Plik ten określa format tłumaczenia poszczególnych elementów języka źródłowego
na język docelowy. Jak widać, nie podano tutaj numerów parametrów wzorców
(np. $1). Wynika to ze szczególnej własności Eli: jeżeli parametry są
używane jednokrotnie we wzorcu, to otrzymują automatycznie numery od 1 wzwyż.
code.lido)Tak jak plik .ptg określa wygląd tekstu wynikowego, plik
.lido określa, co i na jakich zasadach wstawiane jest w miejsce
parametrów w szablonach:
ATTR Ptg: PTGNode;
SYMBOL Entity INHERITS IdPtg END;
SYMBOL text COMPUTE
IF(NoErrors,
PTGOut(
PTGTable(
PTGNumb(CONSTITUENTS set_name.Sym WITH (int, ADD, ARGTOONE, ZERO)),
CONSTITUENTS set_name.Ptg WITH (PTGNode, PTGList, PTGQuoted, PTGNULL),
CONSTITUENTS set_body.Size WITH (PTGNode, PTGList, PTGNumb, PTGNULL),
CONSTITUENTS set_def.Ptg WITH (PTGNode, PTGSeq, IDENTICAL, PTGNULL),
CONSTITUENTS set_name.Ptg WITH (PTGNode, PTGList, PTGName, PTGNULL))));
END;
ATTR Size: int;
SYMBOL set_body COMPUTE
SYNT.Size=CONSTITUENTS set_element.Sym WITH (int, ADD, ARGTOONE, ZERO);
END;
SYMBOL set_def COMPUTE
SYNT.Ptg=
PTGSet(
CONSTITUENT set_name.Ptg,
CONSTITUENTS set_element.Ptg WITH (PTGNode, PTGList, PTGQuoted, PTGNULL));
END;
?sets.specs :exesets.specs :sourcesets.specs :exe :helpsets.specs +debug :dbxsets.specs :exe :err >sets.specs :parsable >sets.specs :exe > transtrans
sets.specs :source > transtrans/
filename <filename
!commandOdinfilePliki o tej nazwie odpowiadają w Eli standardowym plikom Makefile.
Określają one jeden lub więcej obiektów docelowych oraz zasady i źródła, z których
system ma te obiekty zbudować. Oto przykłady takich zasad:
target == sets.specs :exeeli target plik target zostanie w razie potrzeby
uaktualniony
%results == input +command=(mkhdr) :stdout%test ! == . +command=diff (%results) (result) :rundiff na pliku result oraz wyniku
kompilacji (czymkolwiek by on był)
Adt - abstrakcyjne typy danychZaoszczędzają klepania typowych kawałków kodu
LidoList - listy w specyfikacji Lido
List - "zwykłe" listy podanego typu
BitSet - ciągi bitów zmiennej długości
IntSet - ciągi bitów stałej długości (odpowiadają liczbom całkowitym)
Stack
Map - tablica indeksowana innymi elementami
DynSpace - dynamiczna alokacja pamięci
Output - funkcje pomocnicze dla plików wyjściowychPtgCommon - częściej używane funkcje łańcuchowe PTG
Indent - funkcje wspomagające generowanie wcięć w kodzie wynikowym
OutStr - funkcje I/O dla łańcuchów znaków
PrettyPrint - "upiększanie" kodu wynikowego
Prop - przypisanie/sprawdzanie własności obiektówFirstOcc - znajdowanie pierwszego wystąpienia obiektu
Kind - jednoznaczne przypisanie atrybutów do obiektów
KindSet - niejednoznaczne przypisanie atrybutów (obiekt może mieć
przypisany więcej niż jeden atrybut)
ObjCnt - mapowanie obiektów na liczby całkowite
OccCnt - liczenie ilości wystąpień obiektu w regionie
SetFirst - ustawianie atrybutu dla pierwszego wystąpienia danego
obiektu
Unique - sprawdzanie, czy obiekt występuje w regionie jednokrotnie
Clp - przetwarzanie linii poleceńJest to dość specyficzny moduł. Na podstawie specyfikacji składni wywołania programu generuje on funkcje przetwarzające linię poleceń i wypełniające stosowne zmienne. Oto przykład takiej specyfikacji:
CompileOnly "-c" boolean "Compile only, do not link"; MacroPackage "-m" joinedto strings "Load this macro package"; FileName input "File to be processed"; Others positionals "Rest of the parameters";
Z grubsza widać, co się dzieje:
Others
A oto jak używać wygenerowanych opcji we własnym kodzie:
if (FileName!=NoKey) {
printf ("Compilling file %s\n",StringTable(GetClpValue(FileName,0)));
Compile(StringTable(GetClpValue(FileName,0)));
if (!CompileOnly) {
CreateExecutable();
}
}
Dokładniejsze informacje i wyjaśnienie innych opcji można znaleźć w dokumentacji
pakietu clp.
Bardzo oryginalnym pomysłem autorów Eli jest zintegrowanie w jeden plik
(z rozszerzeniem .fw)
formalnego opisu problemu w odpowiednich formatach z jego dokumentacją. Ideologia
jest tu podobna jak np. w wypadku javadoc, tyle, że trochę odwrotnie -
tutaj domyślnie mamy tekst dokumentacji (np. TeX), natomiast pliki źródłowe
są w pliku specjalnie oznaczane. Efekt jest mniej więcej taki:
@p typesetter = tex
@p maximum_input_line_length = infinity
...
\begin{document}
\pagestyle{empty}
...
\title{Tytuł dokumentu}
...
@A@<Introduction@>
...
@O@<sets.ptg@>@{
(tu jest kod pliku źródłowego)
@}
...
i dalej dokumentacja
Oprócz prostych programów wyjaśniających zasadę działania generatora, w pakiecie Eli dostępne są również bardziej zaawansowane aplikacje napisane z jego użyciem:
FORTRAN 77/Fortran 90
Algol 60 - podobnie
C--C - pełna specyfikacja (składnia,gramatyka), lint
Pascal-Pakiet Eli wraz z dokumentacją i przykładami działania jest dostępny pod następującymi adresami: