Hello. In this article, I want to demonstrate how I implemented compilation of mathematical (both numerical and logical) expressions into a delegate using Linq Expression. Navigation: Problem · Compilation rules · Compiler · Default rules · Fancy API · Performance · Examples of compilation · Conclusion · References We want to compile an expression into a function of an arbitrary number of arguments of an arbitrary type, not only numeric, but also boolean. For instance, Since I am doing this within an existing symbolic algebra library, we will immediately proceed to compilation, already having a parser and an expression tree. We have base class Entityclass and its type hierarchy, looking like this: An expression tree is just a graph where the children of a node are the operands of an operator / function. Each type is either abstract (only used to generalize other types) or sealed. The latter can be a real operator / function / constant / other entity that occurs in an expression (be it plus, sine, conjunction, number, set, etc.). For example, this is how the plus operator is defined. This is how I named the interface / data class that will define how Entity's subtypes are mapped to Linq.Expression's structures, operators, or functions. Since we do not know what types the user will request as input and output, the user will have to provide this information. This is what it will look like: Note: We will implement ConstantConverter , BinaryNodeConverterand UnaryNodeConverter at the end of the article. Our compiler will call all these lambdas internally when converting unary nodes, binary nodes, constant nodes, and multiple nodes into Linq.Expression. That is, now we know by what rules we will transform each node. Now it's time to write the "compiler" itself, or rather the algorithm that will build a tree. The prototype for the method we want to write looks like this: Entity expr is an expression that we compile. Type? returnTypeis the return type. We can't extract it from the delegate type, so we have to pass it as a separate argument. CompilationProtocol protocol - this is the protocol of rules by which we will transform each node of the expression. IEnumerable<(Type type, Variable variable)> typesAndNamesis a set of type-variable tuples that the user will to pass to the resulting delegate. For example, if instead of x we ​​want to substitute an integer, and instead of y to pass a complex one, we will writenew[] { (typeof(int), "x"), (typeof(Complex), "y") } And here is the implementation of the method: Its main purpose is to create the necessary containers for the cache of subtrees, local variables, and some other things. The most interesting function here is BuildTree. It will build a linq expression tree from Entity. This is what its prototype looks like: Entity expr - an expression or subexpression to build a tree from. Dictionary<Entity, ParameterExpression> cachedSubexpressions - a dictionary of cached subtrees (that is, those that are already written to existing local variables). List<Expression> variableAssignments - a list of assignments of unique subtrees to local variables. List<ParameterExpression> newLocalVars- local variables, created by BuildTree. (for storing results of subtrees). CompilationProtocol protocol- the rules by which we transform a node Entityinto a node Linq.Expression. It remains unchanged and is simply passed on to all calls of BuildTree. And here is the most important function - BuildTree: I have omitted large chunks of code for the sake of readability. For each subtree, we either immediately return the local variable corresponding to this expression, or we build a new Linq.Expression tree, store it in a new local variable, and return it. Actually, that's about it, the compiler is implemented. But we have not implemented any rules for converting our expressions to Linq.Expression, because we expect these rules to be provided by the user. But why not provide some default rules for built-in types? The rest of the article will be about creating a default protocol. The method itself Compile<TDelegate>(Entity, Type?, CompilationProtocol, IEnumerable<(Type, Variable)>)will be provided to the user, but it is obvious that this is a very long and clumsy construction, you will have to write a huge amount of code describing the transformation of each node and constant, and the method declaration itself is quite long and unclear. So we can provide a default compilation protocol, which will work with some built-in primitives ( bool, int, long, float, double, Complex, BigInteger). ConstantConverter: This rule converts a constant from Entity to Linq.Constant and looks like this: The Entity.Number is casted to a number depending on its type, a boolean constant is unconditionally converted into bool. This function converts Entity.Number to some of the built-in types and is implemented as follows: Returns objectbecause this is exactly what Expression.Constant expects as an argument. This is what we would like to see: we can cast the number to any class, and it's still a constant. UnaryNodeConverter: This protocol's rule is a delegate that converts a node with one argument to a Linq.Expression. I've omitted some big blocks (all code here ). So, here we consider the possible types of our node, and for each we select the desired overload of a function. GetDeffinds the function we want by name. At first I thought of calling all the necessary functions from modules Mathand Complex. I had to write a lot of conditional statments everywhere, consider cases when I should use Math, Complex, and BigInteger. Another issue is that Math does not have some overloads, for instance, int Pow(int, int). Therefore, I created the MathAllMethods class (in T4), where I created all the necessary overloads for all the necessary functions. GetDefsearches for the required method with the given number of arguments and type in this class. This allowed us to get rid of the spaghetti code and write down all calls to the necessary functions by these types in a beautiful and concise manner. BinaryNodeConverter: This rule converts a two-argument node to a Linq.Expression. There is an upcast. Since we may have two expressions of different types, we want to find the most primitive type to which both operands are cast. To do this, I assigned a level to every type: If the types are the same, MaxTypewill return one of them. For example,MaxType(int, int) -> int. If the level of the operand A's type is higher than that of operand B's type, then B is casted to A. For example, MaxType(long, double) -> double. If the levels are equal, but the types are not, then the closest common vertex is found, that is, any such type whose level is higher by 1. For example, MaxType(long, float) -> double. Operands, if necessary, are cast to the selected type, and then we simply find the required overload or operator. For example, for Sumfwe choose Expression.Add, and for conjunction, Andfwill be turned into Expression.And. What happened? Great, we have defined all the necessary rules for our protocol. Now, during creation, we can pass these rules to the required protocol properties. This is the low-level version of what we want to see in the final API: It is inconvenient and requires a lot of work to be performed before we can call one. But we can pass our default protocol AND overload this method for delegates from one argument, two, three, and so on. Since I already assign our rules to the default protocol properties, when passing the protocol, we simply create an instance of it. The second is a little more complicated - I solved it by generating the code using the T4 Text Template. Here's an example of the generated code: It's in the source . We create extension methods in the same way. Here is an example of generated code: Now we need to measure the performance. BenchNormalSimple is a simple lambda, declared right in the code. BenchMySimple is the same lambda, but compiled by me. BenchNormalComplicated is a big fat lambda with a bunch of identical subtrees, declared right in the code. BenchmyComplicated - the same lambda, but compiled by me. Simple things work equally fast, and where there are identical subtrees, my compilation beats the normal one. In general, the result is predictable, and there is nothing extraordinary here. The benchmark is here . (The last example is an example of how the user themselves declares the protocol instead of using the existing one. The result of this compilation is a lambda, concatenating strings). Linq.Expression is truly a brilliant thing. In this short article, we well implemented a compilation process for mathematical expressions. To ensure that the types are arbitrary, we came up with a protocol for translating an expression. To avoid making the user write the same code over and over again, we offer a number of overloads with a default protocol that works great with a number of built-in types. This protocol will automatically upcast types in binary operators and functions to the nearest generic type, if necessary. Such functionality can be used where you would like to get a fast-working mathematical function from a string at runtime. Maybe the community will come up with some other useful application. By the way, I have already used runtime compilation in another project , in which dynamic compilation of nested loops allowed us to avoid recursion (and save precious nanoseconds). Thank you for your attention! The next article will probably be about symbolic limits or parsing from a string. GitHub of the AngouriMath project , within which I developed the compilation Compilation's code here Compilation's tests can be found here Open source contributor