DSL z prawdziwego zdarzenia: projekt generowania zbiorów wyrażeń

Po wprowadzeniu do tematu przekształcania drzewa tokenów języka na drzewo wyrażeń zrozumiałych dla DSLExecutora, czas na implementację. Dzisiaj zajmę się zbiorami wyrażeń, czyli zamianą TokenTree na BatchExpression. Będzie trochę mniej szczegółowo niż zazwyczaj.

W większości dotychczasowych postów pokazywałem fragmenty kodu źródłowego, więc prawdopodobnie już znasz mój styl kodowania, Czytelniku. Dzisiejszy temat pod względem implementacji nie odbiega zbytnio od poprzednich – będzie przetwarzanie tokenów i wyrażeń, będzie rekurencja. Dlatego dzisiaj mniej skupię się na szczegółach, a przejdę poziom wyżej – pokażę projekt klas i ich organizację w przestrzeniach nazw. Implementację też pokażę, ale zwięźle, w pseudokodzie.

Projekt

No dobra, przejdźmy do rzeczy. Chcemy zaimplementować generację wyrażeń na podstawie tokenów. Jest to zagadnienie niezależne od parsowania tekstu, czy innych dotychczas poruszonych zagadnień – a zawsze kiedy wprowadzam nowe zagadnienie, staram się zamknąć je w dedykowanej mu przestrzeni nazw. Niechże przestrzeń dedykowana generowaniu wyrażeń nazywa się ExpressionGeneration.

Ponadto staram się, żeby klasy z innych przestrzeni nazw (reprezentujących inne zagadnienia) nie musiały sięgać głęboko do mojej nowej przestrzeni. Dlatego bezpośrednio w ExpressionGeneration umieszczę klasę, która będzie punktem wejścia do tworzonej funkcjonalności. Idąc za ciosem, niechże klasa ta nazywa się ExpressionGenerator. (W przestrzeni ExpressionGeneration i jej podprzestrzeniach będą oczywiście znajdować się też inne klasy, ale tylko ExpressionGenerator będzie wołany z zewnątrz.)

Co więcej, staram się, aby interfejsy klas były jak najprostsze, jak najbardziej naturalne i jak najmniejsze. Zazwyczaj zawierają pojedynczą metodę. W przypadku klasy ExpressionGenerator prostym i naturalnym interfejsem jest metoda przyjmująca token (typ IToken) i zwracająca wyrażenie (IExpression). Rzutem na taśmę, niechże metoda ta nazywa się Generate.

Idziemy dalej. Spodziewam się, że generowanie wyrażeń poszczególnych typów będzie dość skomplikowane – raczej nie zmieści się w pojedynczej klasie, ani nawet w klasie per typ wyrażenia. Dlatego generację każdego z typów wyrażeń potraktuję jako podzagadnienie godne dedykowanej przestrzeni nazw (a skoro przestrzeni nazw, to i klasy będącej jej punktem wejścia). Przykładowo, generowanie zbiorów wyrażeń realizowane będzie przez klasę BatchExpressionGenerator zamkniętej w przestrzeni BatchExpressionGeneration. Metoda tej klasy (Generate, a jakże) przyjmie TokenTree i zwróci IBatchExpression.

Podsumujmy. Projekt przestrzeni nazw i klas realizujących generowanie wyrażeń przedstawia się następująco:

/ExpressionGeneration
 |__ExpressionGenerator
 |   - IExpression Generate(IToken token)
 |
 |_/BatchExpressionGeneration
 |  |__BatchExpressionGenerator
 |      - IBatchExpression Generate(TokenTree token)
 |
 |_/ConstantExpressionGeneration
 |  |__ConstantExpressionGenerator
 |  |__...
 |
 |_/FunctionExpressionGeneration
    |__FunctionExpressionGenerator
    |__...

Implementacja

Przejdźmy do zarysu implementacji:

ExpressionGenerator

ExpressionGenerator będzie realizował mapowanie tokenów podane w poprzednim poście:

TokenTree to BatchExpression,
FunctionCall to FunctionExpression,
Literal to ConstantExpression.

Do generacji konkretnych wyrażeń użyje klas będących punktami wejścia do odpowiednich podzagadnień. Jego działanie można więc opisać w ten sposób:

  • Jeśli token to Literal, użyj ConstantExpressionGenerator
  • Jeśli token to FunctionCall, użyj FunctionExpressionGenerator
  • Jeśli token to TokenTree, użyj BatchExpressionGenerator
BatchExpressionGenerator

Jak można było się spodziewać, tutaj nie obędzie się bez rekurencji – jako że TokenTree to zbiór tokenów, zamiana go na wyrażenie to zamiana każdego jego tokenu na wyrażenie.

Jak już kilkukrotnie wspominałem, wynik ostatniego wyrażenia stanie się wynikiem całego zbioru wyrażeń. Zatem działanie BatchExpressionExecutor będzie takie:

  • Używając ExpressionGenerator, zamień token.FunctionCalls na wyrażenia
  • Zwróć takie BatchExpression:
{
  ResultExpression = ostatnie wyrażenie
  SideExpressions = reszta wyrażeń
}

Na dzisiaj to tyle, poszło gładko. Implementacja omówionych klas dostępna jest tutaj. To jednak dopiero początek, bo czekają mnie bardziej złożone tematy – przetwarzanie literałów i wywołań funkcji. Zapraszam!