DSL z prawdziwego zdarzenia: generowanie wywołań funkcji

Ostatnio opisałem generator zbiorów wyrażeń, zamieniający TokenTree w BatchExpression. Był to najprostszy z generatorów. Dzisiaj zajmę się bardziej skomplikowanym zadaniem – generowaniem wyrażeń reprezentujących wywołania funkcji, czyli zamianą tokenu FunctionCall na wyrażenie FunctionExpression. Podobnie jak ostatnio, skupię się na projekcie klas implementujących to zagadnienie.

Jak pisałem w poprzednim poście, zagadnienie generowania wyrażeń zamierzam zamknąć w przestrzeni nazw ExpressionGeneration. Natomiast generowanie wyrażeń poszczególnych typów umieszczę w dedykowanych im podprzestrzeniach. Co za tym idzie, generowanie FunctionExpression znajdzie się w podprzestrzeni FunctionExpressionGeneration.

Punktem wejścia do zagadnienia generowania FunctionExpression będzie klasa FunctionExpressionGenerator. Będzie ona przyjmowała token FunctionCall i zwracała odpowiadające mu wygenerowane wyrażenie. Schemat działania tej klasy będzie taki:

  1. Na podstawie nazwy funkcji (token.FunctionName) dowiedz się, która funkcja jest wołana, tj. wyciągnij jej metadane.
  2. Na podstawie tokenów reprezentujących argumenty funkcji (token.Arguments) wygeneruj wyrażenia reprezentujące te argumenty.
  3. Korzystając z uzyskanych danych stwórz obiekt FunctionExpression i go zwróć.

W przedstawionych krokach pojawiają się nowe zagadnienia, tj. wyciąganie metadanych funkcji i generowanie wyrażeń reprezentujących jej argumenty. Klasy realizujące te zagadnienia znajdą się w dedykowanych im przestrzeniach, więc przestrzeń FunctionExpressionGeneration będzie wyglądać tak:

 
/FunctionExpressionGeneration
 |__FunctionExpressionGenerator
 |   - IExpression Generate(FunctionCall functionCall)
 |
 |__/MetadataResolution
 |   - ...
 |
 |__/ArgumentExpressionsGeneration
     - ...

Przyjrzyjmy się zatem wymienionym podzagadnieniom.

Wyciąganie metadanych funkcji

Przez metadane funkcji rozumiem jej typ, parametry i typ zwracany. Podział tych danych na klasy wygląda tak:

 
class FunctionMetadata 
{ 
  Type FunctionType { get; set; } 
  FunctionContract FunctionContract { get; set; } 
} 
 
class FunctionContract 
{ 
  List<FunctionParameterMetadata> Parameters { get; set; } 
  Type ResultType { get; set; } 
}
  
class FunctionParameterMetadata 
{ 
  string Name { get; set; } 
  Type Type { get; set; } 
} 

W krokach działania FunctionExpressionGenerator napisałem, że wyciągnięcie metadanych będzie odbywać się na podstawie nazwy funkcji. Oznacza to, że nie będę wspierał przeciążania funkcji (tworzenia kilku funkcji o tej samej nazwie, różniących się parametrami) – wtedy do rozpoznania wołanej funkcji musiałbym spojrzeć także na listę podanych argumentów.

Tak czy inaczej, potrzebuję mechanizmu, który będzie w stanie zbudować mapę identyfikator funkcji -> metadane tej funkcji. Identyfikatorem w tym przypadku będzie nazwa funkcji, a sam mechanizm niech nazywa się FunctionNameToMetadataMapFactory. Dodatkowo chcę, aby mechanizm ten był łatwo wymienialny, co odzwierciedlę w organizacji przestrzeni nazw.

Kiedy mapa będzie już gotowa, znalezienie metadanych funkcji na podstawie jej identyfikatora będzie proste – wystarczy posłużyć się właśnie mapą. Zajmie się tym klasa FunctionMetadataResolver.

Cała przestrzeń MetadataResolution wygląda tak:

/MetadataResolution
 |__FunctionMetadataResolver
 |   - FunctionMetadata Resolve(FunctionCall functionCall)
 |__IFunctionNameToMetadataMapFactory
 |   - Dictionary<string, FunctionMetadata> Create()
 |
 |__/FunctionNameToMetadataMapFactories
     |__TypeSamplesSuffixConventionBasedMetadataMapFactory

Jak widać, póki co wspieram tylko jedną implementację mechanizmu budowy mapy – TypeSamplesSuffixConventionBasedMetadataMapFactory. Działa ona w następujący sposób:

  1. Przyjmij przykłady typów funkcji – zadaniem użytkownika będzie podanie tu po jednym typie funkcji z każdej biblioteki, w której znajdują się jego funkcje.
  2. Korzystając z tych przykładów uzyskaj zestaw bibliotek, które zawierają typy funkcji.
  3. Przeskanuj wszystkie typy z tych bibliotek w poszukiwaniu typów funkcji (czyli klas implementujących interfejs IFunction<TResult>).
  4. Z każdego typu funkcji wyciągnij metadane tej funkcji. Jak już kilkukrotnie wspominałem:
    1. Parametry funkcji to właściwości występujące w jej typie.
    2. Typ zwracany funkcji to TResult.
  5. Zbuduj słownik nazwa funkcji -> jej metadane i go zwróć:
    1. Nazwa funkcji to nazwa jej typu z usuniętym przyrostkiem Function (np. z typu AddFunction powstanie funkcja Add).

Dobra, bardziej skomplikowana część funkcjonalności za nami. Teraz omówmy drugie podzagadnienie.

Generowanie wyrażeń reprezentujących argumenty funkcji

Tu będzie prościej. Oczywiście pojawi się rekurencja, a jedyną rzeczą wartą wyjaśnienia jest sposób dopasowania argumentów podanych przez użytkownika do parametrów wołanej funkcji. Otóż tak jak w C#, dopasowanie to będzie się odbywać na podstawie kolejności – dla funkcji:

 
class AddFunction 
{ 
  int A { get; set; } 
  int B { get; set; } 
} 

i wywołania:

 
Add('1' '2') 

wartością parametru A będzie 1, a B2. Wydaje się to oczywiste i intuicyjne. Skąd więc w FunctionExpression (i innych miejscach) słownik nazwa parametru -> argument (przecież wystarczyłaby lista argumentów)? Ano stąd, że chcę wesprzeć inne rodzaje składni, np.:

 
call Add
 - B: 2
 - A: 1

No dobrze, przejdźmy do podziału odpowiedzialności pomiędzy klasy. Budową słownika nazwa parametru -> wyrażenie reprezentujące argument zajmie się klasa FunctionArgumentExpressionsGenerator. W tym celu przyjmie ona tokeny reprezentujące argumenty oraz kontrakt funkcji, a do wygenerowania poszczególnych wyrażeń użyje klasy FunctionArgumentExpressionGenerator. Z kolei ta klasa użyje głównej klasy generującej wyrażenia, czyli ExpressionGenerator – i koło się zamyka, mamy rekurencję.

Przestrzeń ArgumentExpressionsGeneration wygląda tak:

/ArgumentExpressionsGeneration
 |__FunctionArgumentExpressionsGenerator
 |   - Dictionary<string, IExpression> Generate
 |       (List<IFunctionArgumentToken> tokens,
 |        FunctionContract functionContract)
 |__FunctionArgumentExpressionGenerator
     - IExpression Generate
         (IFunctionArgumentToken token,
          FunctionParameterMetadata parameterMetadata)

A implementacja całej omawianej dziś funkcjonalności dostępna jest tutaj.

Jestem już naprawdę blisko stworzenia własnego kompilatora. Została tylko implementacja zamiany literałów na wyrażenia reprezentujące wartości stałe (ConstantExpression). Z tym właśnie tematem rozprawię się następnym razem – zapraszam!