Skip to content

Commit

Permalink
Add support of lambda expressions (#140)
Browse files Browse the repository at this point in the history
* Close #25 

* First working prototype.

* Added nested lambda test.

* The lambda expression interpreter inherits the settings from its parent, so that it can access the same variables and types.
Improve resolution of methods where the parameters are generic types.

* Fix issue with lamba expressions where the return type is a generic type (e.g. SelectMany).

* Remove type InferredType, it's not needed.

* Check that a lambda expression has the same number of parameters than the method's parameters.

* Added a parser settings to enable / disable lambda expressions parsing. It's disabled by default because it has a slight performance cost.

* Fix infinite loop when a lambda expression is the sole expression.
Allow a single lambda expression to be converted to a Func type.
Edited documentation.

* Prevent a lambda expression from being matched to a generic parameter.
Only promote a lambda expression if the method's parameter is a delegate with the matching number of generic arguments.

* Remove EnableLambdaExpressions.

* Implement requested changes.

* Fix typo.

* Allow parameterless lambdas.
  • Loading branch information
metoule authored Oct 8, 2021
1 parent b5a22a9 commit ea77214
Show file tree
Hide file tree
Showing 8 changed files with 628 additions and 26 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Dynamic Expresso
# Dynamic Expresso

[![NuGet version](https://badge.fury.io/nu/DynamicExpresso.Core.svg)](http://badge.fury.io/nu/DynamicExpresso.Core)
[![.NET CI](https://github.com/davideicardi/DynamicExpresso/actions/workflows/ci.yml/badge.svg)](https://github.com/davideicardi/DynamicExpresso/actions/workflows/ci.yml)
Expand Down Expand Up @@ -50,6 +50,7 @@ Source code and symbols (.pdb files) for debugging are available on [Symbol Sour
- Good performance compared to other similar projects
- Partial support of generic, params array and extension methods (only with implicit generic arguments detection)
- Partial support of `dynamic` (`ExpandoObject` for get properties, method invocation and indexes(#142), see #72. `DynamicObject` for get properties and indexes, see #142)
- Partial support of lambda expressions (disabled by default, because it has a slight performance penalty)
- Case insensitive expressions (default is case sensitive)
- Ability to discover identifiers (variables, types, parameters) of a given expression
- Small footprint, generated expressions are managed classes, can be unloaded and can be executed in a single appdomain
Expand Down Expand Up @@ -329,6 +330,33 @@ Assert.AreEqual(x.Count(), target.Eval("x.Count()"));
- Generics, only partially supported (only implicit, you cannot invoke an explicit generic method)
- Params array (see C# `params` keyword)

### Lambda expressions
Dynamic Expresso has partial supports of lambda expressions. For example, you can use any Linq method:

```csharp
var x = new string[] { "this", "is", "awesome" };
var options = InterpreterOptions.Default | InterpreterOptions.LambdaExpressions; // enable lambda expressions
var target = new Interpreter(options)
.SetVariable("x", x);

var results = target.Eval<IEnumerable<string>>("x.Where(str => str.Length > 5).Select(str => str.ToUpper())");
Assert.AreEqual(new[] { "AWESOME" }, results);
```

Note that parsing lambda expressions is disabled by default, because it has a slight performance cost.
To enable them, you must set the `InterpreterOptions.LambdaExpressions` flag.

It's also possible to create a delegate directly from a lambda expression:

```csharp
var options = InterpreterOptions.Default | InterpreterOptions.LambdaExpressions; // enable lambda expressions
var target = new Interpreter(options)
.SetVariable("increment", 3); // access a variable from the lambda expression
var myFunc = target.Eval<Func<int, string, string>>("(i, str) => str.ToUpper() + (i + increment)");
Assert.AreEqual("TEST8", lambda.Invoke(5, "test"));
```

### Case sensitive/insensitive
By default all expressions are considered case sensitive (`VARX` is different than `varx`, as in C#).
There is an option to use a case insensitive parser. For example:
Expand All @@ -344,6 +372,8 @@ Assert.AreEqual(x, target.Eval("x", parameters));
Assert.AreEqual(x, target.Eval("X", parameters));
```



## Identifiers detection
Sometimes you need to check which identifiers (variables, types, parameters) are used in expression before parsing it.
Maybe because you want to validate it or you want to ask the user to enter parameters value of a given expression.
Expand Down
26 changes: 25 additions & 1 deletion src/DynamicExpresso.Core/Interpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,21 @@ public Interpreter(InterpreterOptions options)
Reference(LanguageConstants.CommonTypes);
}

if ((options & InterpreterOptions.LambdaExpressions) == InterpreterOptions.LambdaExpressions)
{
_settings.LambdaExpressions = true;
}

_visitors.Add(new DisableReflectionVisitor());
}

/// <summary>
/// Create a new interpreter with the settings copied from another interpreter
/// </summary>
internal Interpreter(ParserSettings settings)
{
_settings = settings;
}
#endregion

#region Properties
Expand Down Expand Up @@ -379,9 +392,20 @@ public Expression<TDelegate> ParseAsExpression<TDelegate>(string expressionText,
return lambda.LambdaExpression<TDelegate>();
}

internal LambdaExpression ParseAsExpression(Type delegateType, string expressionText, params string[] parametersNames)
{
var lambda = ParseAs(delegateType, expressionText, parametersNames);
return lambda.LambdaExpression(delegateType);
}

public Lambda ParseAs<TDelegate>(string expressionText, params string[] parametersNames)
{
var delegateInfo = ReflectionExtensions.GetDelegateInfo(typeof(TDelegate), parametersNames);
return ParseAs(typeof(TDelegate), expressionText, parametersNames);
}

internal Lambda ParseAs(Type delegateType, string expressionText, params string[] parametersNames)
{
var delegateInfo = ReflectionExtensions.GetDelegateInfo(delegateType, parametersNames);

return ParseAsLambda(expressionText, delegateInfo.ReturnType, delegateInfo.Parameters);
}
Expand Down
4 changes: 4 additions & 0 deletions src/DynamicExpresso.Core/InterpreterOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ public enum InterpreterOptions
/// </summary>
LateBindObject = 16,
/// <summary>
/// Enable parsing of lambda expressions. Disabled by default, because it has a slight performance cost.
/// </summary>
LambdaExpressions = 32,
/// <summary>
/// Load all default configurations: PrimitiveTypes + SystemKeywords + CommonTypes
/// </summary>
Default = PrimitiveTypes | SystemKeywords | CommonTypes,
Expand Down
19 changes: 15 additions & 4 deletions src/DynamicExpresso.Core/Lambda.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.ExceptionServices;
using DynamicExpresso.Exceptions;

namespace DynamicExpresso
{
Expand Down Expand Up @@ -72,9 +71,9 @@ public object Invoke(params Parameter[] parameters)
public object Invoke(IEnumerable<Parameter> parameters)
{
var args = (from usedParameter in UsedParameters
from actualParameter in parameters
where usedParameter.Name.Equals(actualParameter.Name, _parserArguments.Settings.KeyComparison)
select actualParameter.Value)
from actualParameter in parameters
where usedParameter.Name.Equals(actualParameter.Name, _parserArguments.Settings.KeyComparison)
select actualParameter.Value)
.ToArray();

return InvokeWithUsedParameters(args);
Expand Down Expand Up @@ -155,5 +154,17 @@ public Expression<TDelegate> LambdaExpression<TDelegate>()
{
return Expression.Lambda<TDelegate>(_expression, DeclaredParameters.Select(p => p.Expression).ToArray());
}

internal LambdaExpression LambdaExpression(Type delegateType)
{
var types = delegateType.GetGenericArguments();

// return type
types[types.Length - 1] = _expression.Type;

var genericType = delegateType.GetGenericTypeDefinition();
var inferredDelegateType = genericType.MakeGenericType(types);
return Expression.Lambda(inferredDelegateType, _expression, DeclaredParameters.Select(p => p.Expression).ToArray());
}
}
}
Loading

0 comments on commit ea77214

Please sign in to comment.