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:
- Na podstawie nazwy funkcji (token.FunctionName) dowiedz się, która funkcja jest wołana, tj. wyciągnij jej metadane.
- Na podstawie tokenów reprezentujących argumenty funkcji (token.Arguments) wygeneruj wyrażenia reprezentujące te argumenty.
- 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:
- 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.
- Korzystając z tych przykładów uzyskaj zestaw bibliotek, które zawierają typy funkcji.
- Przeskanuj wszystkie typy z tych bibliotek w poszukiwaniu typów funkcji (czyli klas implementujących interfejs IFunction<TResult>).
- Z każdego typu funkcji wyciągnij metadane tej funkcji. Jak już kilkukrotnie wspominałem:
- Parametry funkcji to właściwości występujące w jej typie.
- Typ zwracany funkcji to TResult.
- Zbuduj słownik nazwa funkcji -> jej metadane i go zwróć:
- 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 B – 2. 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!