Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

mgrzeg.net - Admin on Rails :)

MPLEX - Lex dla Visual Studio

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:

>mplex ex0.lex

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:

  1. 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;);
  2. Reguł - w tym miejscu umieszczamy wzorce oraz kod C#, który jest przetwarzany w momencie wystąpienia danego wzorca;
  3. 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:

  1. przy przejściu do nowego wiersza przechodzimy do stanu wyszukiwania słowa w słowniku;
  2. po pojawieniu się na początku wiersza słowa rzeczownik, czasownik, lub przymiotnik przechodzimy do odpowiadającego mu stanu;
  3. 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ć :(

Opublikowane 27 lipca 2008 03:01 przez mgrzeg

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

 

Wojciech Gebczyk said:

1. Pod wzgledem kodowania tekstu (encoding, code page, etc), to niestet nie jest za rozowo. Wspierny jest ASCII i UTF (chyba 8 o ile pamietam). Czasami to za malo, a czasami nie. Bierze sie to stad, ze xPLEX uzywa Stream (i recznie rozpoznaje kodowanie) a nie TextReadera. W pewnych zastosowaniach to duza slabosc.

2. Wydajnosc dla duuuzych plikow jest za slaba niz reczny parser/lekser. Pliki o dziesiatkach/ setkach MB wolniej sie parsuja, niz takowe "wczytyane" recznym paserem/lexerem.

3. Zmiana recznego leksera i parsera (w sumie kolo 4 klas z 20-30 metodami oraz okolo 90 metod testowych), zastapilem 2 plikami z gramatyka oraz 2 plikami .cs wygenerowanymi. Dodatkowo latwiejsze pozniejsze zmiany byly. Kostem szybkosci - 3-4 krotne wolniejsze przetwarzanie.

w sumie warto bylo tego uzyc, mimo ze troche balaganirski i nie fektywny kod produkuje :-) raczej polecam tool, a zupelnie polecam do plikow do paru MB

lipca 27, 2008 21:47
 

mgrzeg said:

Wojtek, liczylem na Twoj komentarz! :)

Ad 1. - zgadza sie, ba, nawet specjalnie dalem przyklad z 'ładnego', zeby bylo widac, ze cos jest nie tak. Nie opisywalem tego dokladniej, bo czekalem az ktos sie odezwie i spyta "a dlaczego nie ma 'ł'?", a ja wtedy moglbym powiedziec, ze w mplexie do vs2k8 i nastepnych (az do obecnego 0.62 gplex) nastapila poprawa co do obslugi unicode i nie tylko i autorzy sie tym chwala :) Oczywiscie wszystko wychodzi w praniu i czy jest bardziej rozowo niz bylo, to sie dopiero okaze.

Co do pozostalych uwag - trzeba pamietac jaki jest glowny cel tego toola. Ale o tym w nastepnych odcinkach :)

lipca 27, 2008 22:57
 

Wojciech Gebczyk said:

Michal, co do kodowania, to dziwi mnie uzycie Stream jako bazy w przetwarzaniu (sa inne inputy, ale bazuja na czymsiach wczytancych do pamieci). .NET ma wbudowana obsluge, ASCII, code pages, UTF wszelakiej masci. Czemu nie mogli tego bazowac na TextReaderze z wlasnym opakowywaczem na czytacza? Probowalem jakis czas temu zmeinic na TextRedera i okazalo sie ze roba tam (w xPLEX) nagminnie "seeki" - slaabe to jest troche.

MPLEX sprawdza sie swietnie jako czesc Managed Babel i calej tej infrastruktury [nie bede rozwijal bo pewnie to jest cel tego textu :-) ]

BTW: Jesli bedziesz wiedzial jak zrobic elegancko "przerwanie" parsowania, napisz! (dokladniej chodzi mi o to aby na przyklad wczytac pierwxsze 100 tokenow i przerwac dzialanie)

lipca 27, 2008 23:32
 

mgrzeg said:

Heh, Wojtek, mnie tez szlag trafial na ten ich Stream. Najlepsze jest to, ze we wlasnym przykladzie kalkulatora skorzystali z TextReadera piszac recznie scanner.

No niestety, przeznaczenie tego narzedzia jest jakie jest i pewnie wydawalo im sie celowe uzycie Stream. :(

lipca 28, 2008 01:33
 

mgrzeg.net - Admin on Rails :) said:

Poprzednio pisałem o MPLex, czas na kilka słów o MPPG. Zobaczymy przy okazji jak można skorzystać z obu tych narzędzi do opisywania prostych gramatyk.

lipca 28, 2008 02:59
 

mgrzeg.net - Admin on Rails :) said:

Biblijna wieża Babel w Visual Studio? No cóż, jeśli spojrzeć na Visual Studio jako kombajn, który dostarcza

sierpnia 6, 2008 01:26

Co o tym myślisz?

(wymagane) 
(opcjonalne)
(wymagane) 

  
Wprowadź kod: (wymagane)
Wyślij
W oparciu o Community Server (Personal Edition), Telligent Systems