Intro
Dziś będzie trochę o zamierzchłych czasach, gdy dotnet jeszcze nie był nawet w planach, ludzie zamiast Tuwima czytali wiersze poleceń, a na świecie panowały narzędzia konsolowe. Czasy się zmieniły, niektóre ze starszych narzędzi wymarły, inne przybrały nową postać, ale część z nich pozostała i dobrze wpasowała się w nową sytuację. Jednym z takich narzędzi jest unixowy lex. Oryginalnie lex na podstawie reguł opisanych przez wyrażenia regularne tworzył kod leksera w języku C, który można było dalej wykorzystać przez narzędzia typu yacc do tworzenia bardziej skomplikowanych narzędzi - kompilatorów. O yaccu jeszcze przyjdzie coś powiedzieć, teraz jednak skupmy się na lexie. Oczywiście lekser w C jest raczej mało przydatny w środowisku .NET i najfajniej byłoby mieć narzędzie, które na podstawie dostarczonego przez nas pliku z regułami leksykalnymi wygeneruje kod w C#. Na szczęście Microsoft wspiera od czasu do czasu pewne projekty akademickie i dzięki temu w SDK do VS pojawił się projekt Managed Babel, oparty o projekty GPPG oraz GPLEX, czyli parsera i leksera na platformę .NET. Tak oto dotarliśmy do miejsca, w którym czas najwyższy przedstawić MPLex - Managed Package Lex. Do zabawy użyjemy mplex z SDK do VS2005 z lutego 2007.
Gdzie jest mplex?
MPLex wraz z innymi przydatnymi narzędziami z SDK pojawia się w naszym systemie po zainstalowaniu VS2005 (wersja Standard lub wyższa) wraz z SP1 oraz SDK 4.0 do VS. Znaleźć go możemy w katalogu "%VS2K5SDK%\ 2007.02\VisualStudioIntegration\Tools\Bin", gdzie %VS2K5SDK% na moim komputerze to "C:\Program Files\Visual Studio 2005 SDK". Dla wygody proponuję dorzucić w tym momencie ścieżkę to katalogu z mplexem do zmiennej systemowej PATH. W kilku następnych przykładach będziemy używali cmd, więc warto ułatwić sobie dostęp do tego narzędzia z dowolnego miejsca na dysku. Dodatkowo, w katalogu "%VS2K5SDK%\2007.02\VisualStudioIntegration\ExtraDocumentation" znajdują się 3 pdfy z dokumentacją do pakietu Babel oraz narzędzi MPLex i MPPG.
Przy okazji - ja czasem potrzebuję mieć trochę więcej, niż tylko cmd i mplex w ścieżce, więc najwygodniej jest mi uruchomić cmd VS2k5 w danym katalogu wprost z explorera, w czym pomaga mi odpowiedni wpis w rejestrze (xp):
------vs2k5.reg-----
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\Folder\shell\VS2K5 cmd]
[HKEY_CLASSES_ROOT\Folder\shell\VS2K5 cmd\command]
@="cmd.exe /k \"C:\\Progra~1\\MICROS~4\\VC\\VCVARS~1.BAT x86\""
------vs2k5.reg-----
gdzie "MICROS~4" to "Microsoft Visual Studio 8.0" (dir /x).
Oczywiście podobnie można ustawić sobie cmd z explorera dla innych wersji VS.
Przykłady
Zacznijmy od najprostszych przykładów, bez większego wgłębiania się w składnię lexa.
1. ex0.lex
/* SEKCJA 1: DEFINICJE */
%namespace LexScanner
%%
/* SEKCJA 2: REGULY */
.|\n ECHO();
%%
/* SEKCJA 3: KOD UZYTKOWNIKA */
public static void Main(string [] args) {}
Teraz przechodzimy do wiersza poleceń i w katalogu z plikiem ex0.lex wykonujemy:
Po wykonaniu tego polecenia w katalogu zawierającym ex0.lex powinien pojawić się dodatkowo plik ex0.cs z wygenerowanym scannerem.
Dorzućmy do tego plik dummy.cs (z dokumentacji do MPLex):
//dummy.cs
using System;
namespace LexScanner
{
public class Tokens
{
public const int EOF = 0;
public const int maxParseToken = int.MaxValue;
}
public abstract class ScanBase
{
protected int currentScOrd;
public virtual int
GetEolState() { return currentScOrd; }
public virtual void
SetEolState(int value) { currentScOrd = value; }
public abstract void
SetSource(string s, int o);
public abstract int
GetNext(ref int state, out int start, out int end);
public abstract int yylex();
}
public interface IErrorHandler
{
int ErrNum { get; } int WrnNum { get; }
void AddError(string msg,
int lin, int col, int len, int severity);
}
}
i będąc dalej w wierszu poleceń wykonajmy:
>csc /out:ex0.exe ex0.cs dummy.cs
Jeśli nie popełniliśmy po drodze żadnego błędu, to w tym momencie powinniśmy uzyskać program ex0.exe, który uruchomiony... nic nie robi :)
Gratuluję, oto spędziłeś(aś) właśnie cały wieczór na przygotowaniu programu, który nic nie robi :). Ale nie przejmuj się. Zmodyfikujmy jednak nieco kod Main():
2. ex1.lex
/* SEKCJA 1: DEFINICJE */
%namespace LexScanner
%%
/* SEKCJA 2: REGULY */
.|\n ECHO();
%%
/* SEKCJA 3: KOD UZYTKOWNIKA */
public static void Main(string [] args) {
Scanner scnr = new Scanner();
string line = Console.ReadLine();
int tok;
do {
scnr.SetSource(line, 0);
tok = scnr.yylex();
Console.WriteLine();
} while((line = Console.ReadLine()) != null);
}
i teraz nasz wynikowy program powinien wypluwać na konsolę to, co wprowadzimy z klawiatury.
3. ex2.lex.
Tym razem klasyczny już przykład licznika słów, wierszy i znaków w pliku:
/* SEKCJA 1: DEFINICJE */
%namespace LexScanner
%{
static int lineTot = 0;
static int wordTot = 0;
static int charTot = 0;
%}
word [^ \t\r\n]+
eol [\n]
%%
/* SEKCJA 2: REGULY */
%{
int lines = 0;
int words = 0;
int chars = 0;
%}
{word} {words++; chars += yyleng;}
{eol} {lines++; chars += yyleng;}
. {chars++;}
<<EOF>> {
Console.Write("wierszy: " + lines); lineTot += lines;
Console.Write(", slow: " + words); wordTot += words;
Console.WriteLine(", znakow: " + chars); charTot += chars;
}
%%
/* SEKCJA 3: KOD UZYTKOWNIKA */
public static void Main(string[] argp)
{
for (int i = 0; i < argp.Length; i++)
{
string name = argp[i];
try
{
int tok;
FileStream file = new FileStream(name, FileMode.Open);
Scanner scnr = new Scanner(file);
Console.WriteLine("Plik: " + name);
do
{
tok = scnr.yylex();
} while (tok > Tokens.EOF);
}
catch (IOException ex)
{
Console.WriteLine(ex.Message);
}
}
if (argp.Length > 1)
{
Console.Write("Wierszy w sumie: " + lineTot);
Console.Write(", Slow: " + wordTot);
Console.WriteLine(", Znakow: " + charTot);
}
}
Po wygenerowaniu skanera oraz skompilowaniu programu i jego uruchomieniu otrzymujemy dla przykładu:
>ex2.exe ex0.lex ex1.lex ex2.lex
Plik: ex0.lex
wierszy: 8, slow: 29, znakow: 181
Plik: ex1.lex
wierszy: 16, slow: 55, znakow: 417
Plik: ex2.lex
wierszy: 57, slow: 174, znakow: 1207
Wierszy w sumie: 81, Slow: 258, Znakow: 1805
Po tym przykładzie czas na chwilę refleksji.
Plik mplex-a składa się z trzech sekcji:
- Definicji - tu, jak sama nazwa wskazuje :), jest miejsce na definicje, które później można wykorzystać w pozostałych sekcjach. Tu ustalamy przestrzeń, w jakiej ma znaleźć się klasa skanera (%namespace LexScanner), możemy też dodawać odpowiednie usingi (%using System.Text;);
- Reguł - w tym miejscu umieszczamy wzorce oraz kod C#, który jest przetwarzany w momencie wystąpienia danego wzorca;
- Kodu użytkownika - tu jest najlepsze miejsce na zdefiniowanie niektórych metod pomocniczych oraz głównej metody: Main.
W sekcjach definicji oraz reguł możemy dodawać dodatkowy kod C#, który będzie umieszczony w klasie skanera w pliku wynikowym. Kod taki rozpoczynamy sekwencją "%{", a zamykamy "%}". I tak - jeśli dodatkowy kod umieścimy w sekcji Definicji, to pojawi się on w definicji klasy Scanner, natomiast jeśli umieścimy nasz kod w sekcji Reguł, to zostanie on wstawiony do metody Scan, czyli głównej metody skanera, wołanej przez metodę yylex().
W pliku dummy.cs zdefiniowana jest klasa abstrakcyjna ScanBase, która implementowana jest przez klasę Scanner, wygenerowaną przez MPLex w wyniku przetwarzania naszego pliku .lex. Dodatkowo pojawia się również definicja interfejsu IErrorHandler, o którym powiemy więcej przy omawianiu MPPG.
Zauważmy, że w definicjach wzorców użyte są wyrażenia regularne, które wydatnie upraszczają całą zabawę. Warto zwrócić także uwagę na specjalny symbol <<EOF>>, który określa koniec pliku (w oryginalnym lex do obsługi końca pliku wykorzystywana była funkcja yywrap).
Na koniec stosunkowo prosty przykład, który wykorzystamy w dalszych zabawach z VS.
4. ex3.lex
/* SEKCJA 1: DEFINICJE */
%using System.Collections;
%namespace LexScanner
%{
public enum Locals {
LOOKUP = 0,
RZECZOWNIK,
CZASOWNIK,
PRZYMIOTNIK
};
Locals stan;
Hashtable words = new Hashtable();
internal void add_word(Locals s, string text) {
if(!words.Contains(text))
words.Add(text, s);
}
internal Locals lookup_word(string text) {
if(words.Contains(text)) return (Locals) words[text];
else return Locals.LOOKUP;
}
%}
%%
/* SEKCJA 2: REGULY */
\n {stan = Locals.LOOKUP;}
^rzeczownik {stan = Locals.RZECZOWNIK;}
^czasownik {stan = Locals.CZASOWNIK;}
^przymiotnik {stan = Locals.PRZYMIOTNIK;}
[a-zA-Z]+ {
if(stan != Locals.LOOKUP) add_word(stan, yytext);
else {
switch(lookup_word(yytext)) {
case Locals.RZECZOWNIK: Console.WriteLine("{0}: RZECZOWNIK!", yytext);
break;
case Locals.CZASOWNIK: Console.WriteLine("{0}: CZASOWNIK!", yytext);
break;
case Locals.PRZYMIOTNIK: Console.WriteLine("{0}: PRZYMIOTNIK!", yytext);
break;
default: Console.WriteLine("{0}: zdefiniuj, bo nie znam :(", yytext);
break;
}
}
}
%%
/* SEKCJA 3: KOD UZYTKOWNIKA */
public static void Main(string[] args) {
Scanner scnr = new Scanner();
string line = Console.ReadLine();
int tok;
do {
scnr.SetSource(line, 0);
tok = scnr.yylex();
} while((line = Console.ReadLine()) != null);
}
Przykładowe wykonanie programu:
>ex3.exe
ala ma ładnego kota
ala: zdefiniuj, bo nie znam :(
ma: zdefiniuj, bo nie znam :(
adnego: zdefiniuj, bo nie znam :(
kota: zdefiniuj, bo nie znam :(
rzeczownik ala kota
czasownik ma
przymiotnik ładnego
ala ma ładnego kota
ala: RZECZOWNIK!
ma: CZASOWNIK!
adnego: PRZYMIOTNIK!
kota: RZECZOWNIK!
W sekcji Definicji tworzymy mały słownik, w którym będziemy przechowywali interesujące nas części mowy. Przy okazji definiujemy typ wyliczeniowy Locals, którym będziemy operować przy określaniu stanu w jakim aktualnie się znajdujemy
W sekcji Reguł ustalamy:
- przy przejściu do nowego wiersza przechodzimy do stanu wyszukiwania słowa w słowniku;
- po pojawieniu się na początku wiersza słowa rzeczownik, czasownik, lub przymiotnik przechodzimy do odpowiadającego mu stanu;
- w pozostałych przypadkach, zależnie od stanu w jakim aktualnie się znajdujemy, albo dodajemy nowe słowo do słownika, albo wyszukujemy w słowniku pojawiające się słowo.
Kod użytkownika jest bardzo podobny do tego z przykładu ex1.lex, usunięte zostało tylko niepotrzebne przechodzenie do nowego wiersza.
Trzy słowa na zakończenie
Jak widać, zabawa z MPLexem jest całkiem prosta, a efekty mogą być naprawdę zaskakujące. Tworzenie własnego programu do analizy leksykalnej z wykorzystaniem MPLex jest zdecydowanie prostsze niż pisanie go ręcznie, a wszystko to jest dostępne w ukochanym C# ;).
Oczywiście, MPLex nie jest jedyną implementacją unixowego lexa generującego kod w C#, jednak jest to jedyne narzędzie wspierane przez Microsoft i dedykowane do zabawy z usługami językowymi Visual Studio. Dlatego nie ma sensu omawianie innych narzędzi tej klasy :(, chętnych zapraszam do google.com :).
I już na samo zakończenie słowo wyjaśnienia, dlaczego skupiam się na MPLex z SDK do VS2k5, a nie do VS2k8. Otóż niestety wersja 0.60 GPLEX, która była podstawą do MPLEX z SDK do VS2k8 wprowadza błędy, które nie pozwalają nawet na przejście powyższych przykładów :(. Podobnie ma się sprawa z wersją 0.61 i dopiero w wersji 0.62 zostało to usunięte. Jak przypuszczam, SDK v.2 do VS2k8 wykorzysta najnowszą wersję GPLEX, w którym m.in. klasa Scanner może być już partial, co oczywiście pozwala na łatwiejsze zarządzanie kodem; dodatkowo nie są już wymagane dodatkowe definicje klasy bazowej skanera, jest też wiele innych przyjemnych nowości, ale szkoda czasu na dalsze dywagacje na temat narzędzia, z którego na razie nie ma jak sensownie skorzystać :(