Przyszedł czas na ostateczną rozprawę z tematem kompilacji przykładowego DSLa – dzisiaj omówię implementację parsowania literałów i przeistaczania ich w wyrażenia. Tym samym zamknę minimalny planowany zakres funkcjonalności DSLExecutora. I dobrze, bo do końca konkursu zostały równo dwa tygodnie!
Ale do rzeczy. Dzisiaj zajmę się zamianą tokenu Literal na wyrażenie ConstantExpression. Początkowo zakładałem, że implementacja tej części kompilatora będzie dość trudna – chcąc uniknąć karkołomnej dłubaniny przy parserze literałów, postanowiłem, że ich składnia będzie jak najprostsza, a trudne będzie właśnie generowanie z nich wyrażeń. Trudność miała polegać na rozpoznaniu typu literału (np. rozpoznaniu, że 1 to liczba, a nie napis).
Parser
Okazało się jednak, że z biblioteką Sprache parsowanie to czysta przyjemność! [Uwaga: Post zawiera lokowanie produktu.] Wobec tego zdecydowałem, że składnia literałów mojego języka będzie przypominać tą z C# (i masy innych języków). Dla zachowania prostoty, na razie przewiduję wsparcie dla czterech typów literałów:
- wartość logiczna (bool), czyli true / false,
- liczba całkowita (int), np. 123,
- liczba ułamkowa (double), np. 123.456,
- napis (string) ujęty w apostrofy, np. 'text'.
Oprócz tego, wypadałoby jakoś wesprzeć brak jakiejkolwiek wartości – będzie go reprezentowało słowo kluczowe null.
A oto implementacja parserów wszystkich tych literałów (oprócz parsera napisów, bo już go pokazywałem):
null
NullParser = Parse.String("null") .Select(x => (object)null) .Token();
bool
BoolParser = Parse.String("true").Select(x => true) .Or(Parse.String("false") .Select(x => false)) .Token();
int
IntParser = Parse.Digit.AtLeastOnce().Text() .Select(int.Parse).Token();
double
DoubleParser = (from intPart in Parse.Digit.AtLeastOnce() from sep in Parse.Char('.') .Select(x => new[] { x }) from fractPart in Parse.Digit.AtLeastOnce() select new string(intPart.Concat(sep) .Concat(fractPart) .ToArray()) ).Select(double.Parse) .Token();
Proste, prawda? Teraz implementacja zbiorczego parsera wszystkich literałów:
LiteralParser = NullParser .Or(BoolParser.Select(x => (object)x)) .Or(DoubleParser.Select(x => (object)x)) .Or(IntParser.Select(x => (object)x)) .Or(StringParser.Select(x => (object)x)) .Select(x => new Literal { Value = x }) .Token();
Zwróć uwagę, Czytelniku, że kolejność użycia pojedynczych parserów ma tutaj znaczenie – gdyby IntParser znalazł się przed DoubleParser, to ciąg 1.1 zostałby sparsowany jako int o wartości 1, a pozostała część ciągu (.1) spowodowałaby błąd parsowania (żaden token nie zaczyna się od znaku .).
Generator
Pora na generator, czyli klasę ConstantExpressionGenerator, implementującą wspomnianą wcześniej zamianę tokenu Literal na wyrażenie ConstantExpression. Mechanizm jej działania jest banalny:
- Sprawdź typ wartości literału.
- Zwróć ConstantExpression<TResult>, gdzie TResult to typ wartości literału. Jako wartość (właściwość Value) wyrażenia podaj wartość literału.
I gotowe – cykl “DSL z prawdziwego zdarzenia” uważam za zamknięty! DSLExecutor jest w stanie przyjąć tekst, np.
log('Calculating...') sub(add(1 2) 3)
, sparsować go, wykonać (o ile załadowana biblioteka standardowa zawiera funkcje add, sub i log) i zwrócić wynik (tutaj: 0). Czy można to nazwać kompilacją kodu źródłowego (wejściowego DSLa) i uruchomieniem wygenerowanego programu (zestawu wyrażeń)? Załóżmy, że tak
Skoro DSLExecutor już działa, to następnym krokiem będzie osadzenie go w apce webowej i umożliwienie Czytelnikowi własnoręcznego pobawienia się nim – stay tuned!