Skip to content

Commit

Permalink
Issues 212, 256: Adding support for internal lambdas to reference par…
Browse files Browse the repository at this point in the history
…ameters from the parent lambda (#257)

* Issues 212, 256: Adding support for internal lambdas to reference parameters from the parent lambda.

* Added additional check to ensure we can't have duplicate parameter names in a child lambda scope.

* Fixing a unit test that had a bad lamda parameter declaration.

Co-authored-by: Holden Mai <Holden.Mai@cssregtech.com>
  • Loading branch information
holdenmai and Holden Mai authored Sep 21, 2022
1 parent f0cbd31 commit 7db3e48
Show file tree
Hide file tree
Showing 3 changed files with 415 additions and 14 deletions.
55 changes: 42 additions & 13 deletions src/DynamicExpresso.Core/Parsing/Parser.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -138,7 +139,7 @@ private Expression ParseLambdaExpression()
{
// in case the expression is not a lambda, we have to restart parsing
var originalPos = _token.pos;

var isLambda = false;
try
{
var parameters = ParseLambdaParameterList();
Expand Down Expand Up @@ -168,10 +169,15 @@ private Expression ParseLambdaExpression()
}

var lambdaBodyExp = _expressionText.Substring(startExpr, _token.pos - startExpr);
isLambda = true;
return new InterpreterExpression(_arguments, lambdaBodyExp, parameters);
}
catch (ParseException)
{
if (isLambda)
{
throw;
}
// not a lambda, return to the saved position
SetTextPos(originalPos);
NextToken();
Expand All @@ -180,13 +186,24 @@ private Expression ParseLambdaExpression()
}
}

private Parameter[] ParseLambdaParameterList()
private class ParameterWithPosition : Parameter
{
public ParameterWithPosition(int pos, string name, Type type)
: base(name, type)
{
Position = pos;
}

public int Position { get; }
}

private ParameterWithPosition[] ParseLambdaParameterList()
{
var hasOpenParen = _token.id == TokenId.OpenParen;
if (hasOpenParen)
NextToken();

var parameters = _token.id != TokenId.CloseParen ? ParseLambdaParameters() : new Parameter[0];
var parameters = _token.id != TokenId.CloseParen ? ParseLambdaParameters() : new ParameterWithPosition[0];
if (hasOpenParen)
{
ValidateToken(TokenId.CloseParen, ErrorMessages.CloseParenOrCommaExpected);
Expand All @@ -196,9 +213,9 @@ private Parameter[] ParseLambdaParameterList()
return parameters;
}

private Parameter[] ParseLambdaParameters()
private ParameterWithPosition[] ParseLambdaParameters()
{
var argList = new List<Parameter>();
var argList = new List<ParameterWithPosition>();
while (true)
{
argList.Add(ParseLambdaParameter());
Expand All @@ -208,11 +225,12 @@ private Parameter[] ParseLambdaParameters()
return argList.ToArray();
}

private Parameter ParseLambdaParameter()
private ParameterWithPosition ParseLambdaParameter()
{
ValidateToken(TokenId.Identifier);
var name = _token.text;

var pos = _token.pos;
if (TryParseKnownType(name, out var type))
{
ValidateToken(TokenId.Identifier);
Expand All @@ -224,7 +242,7 @@ private Parameter ParseLambdaParameter()
}

NextToken();
return new Parameter(name, type);
return new ParameterWithPosition(pos, name, type);
}

// = operator
Expand Down Expand Up @@ -1229,7 +1247,6 @@ private MemberBinding[] ParseMemberInitializerList(Type newType)
var member = FindPropertyOrField(newType, propertyOrFieldName, false);
if (member == null)
throw CreateParseException(_token.pos, ErrorMessages.UnknownPropertyOrField, propertyOrFieldName, GetTypeName(newType));

NextToken();

ValidateToken(TokenId.Equal, ErrorMessages.EqualExpected);
Expand Down Expand Up @@ -3249,17 +3266,29 @@ private class InterpreterExpression : Expression
private readonly IList<Parameter> _parameters;
private Type _type;

public InterpreterExpression(ParserArguments parserArguments, string expressionText, params Parameter[] parameters)
public InterpreterExpression(ParserArguments parserArguments, string expressionText, params ParameterWithPosition[] parameters)
{
_interpreter = new Interpreter(parserArguments.Settings.Clone());
var settings = parserArguments.Settings.Clone();
_interpreter = new Interpreter(settings);
_expressionText = expressionText;
_parameters = parameters;

// convert the parent's parameters to variables
// Take the parent expression's parameters and set them as an identifier that
// can be accessed by any lower call
// note: this doesn't impact the initial settings, because they're cloned
foreach (var pe in parserArguments.DeclaredParameters)
foreach (var dp in parserArguments.DeclaredParameters)
{
_interpreter.SetVariable(pe.Name, pe.Value, pe.Type);
// Have to mark the parameter as "Used" otherwise we can get a compilation error.
parserArguments.TryGetParameters(dp.Name, out var pe);
_interpreter.SetIdentifier(new Identifier(dp.Name, pe));
}

foreach (var myParameter in parameters)
{
if (settings.Identifiers.ContainsKey(myParameter.Name))
{
throw new ParseException($"A local or parameter named '{myParameter.Name}' cannot be declared in this scope because that name is used in an enclosing local scope to define a local or parameter", myParameter.Position);
}
}

// prior to evaluation, we don't know the generic arguments types
Expand Down
86 changes: 86 additions & 0 deletions test/DynamicExpresso.UnitTest/GithubIssues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,92 @@ public void GitHub_Issue_235()
var result2 = target.Eval<DateTimeKind>("DateTimeKind.Local | DateTimeKind.Utc");
Assert.AreEqual((DateTimeKind)3, result2);
}

[Test]
public void GitHub_Issue_212()
{
var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions);
var list = new Parameter("list", new[] { 1, 2, 3 });
var value1 = new Parameter("value", 1);
var value2 = new Parameter("value", 2);
var expression = "list.Where(x => x > value)";
var lambda = target.Parse(expression, list, value1);
var result = lambda.Invoke(list, value2);
Assert.AreEqual(new[] { 3 }, result);
}

[Test]
public void GitHub_Issue_212_bis()
{
var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions);
var list = new Parameter("list", new[] { 1, 2, 3 });
var value1 = new Parameter("value", 1);
var value2 = new Parameter("value", 2);
var expression = "list.Where(x => x > value)";
var lambda = target.Parse(expression, (new[] { list, value1 }).Select(p => new Parameter(p.Name, p.Type)).ToArray());
var result = lambda.Invoke(list, value1);
Assert.AreEqual(new[] { 2, 3 }, result);
}

[Test]
public void GitHub_Issue_200_capture()
{
var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions);
var list = new List<string> { "ab", "cdc" };
target.SetVariable("myList", list);

// the str parameter is captured, and can be used in the nested lambda
var results = target.Eval("myList.Select(str => str.Select(c => str.Length))");
Assert.AreEqual(new[] { new[] { 2, 2 }, new[] { 3, 3, 3 } }, results);
}

[Test]
public void Lambda_Issue_256()
{
ICollection<BonusMatrix> annualBonus = new List<BonusMatrix> {
new BonusMatrix() { Grade = 1, BonusFactor = 7 },
new BonusMatrix() { Grade = 2, BonusFactor = 5.5 },
new BonusMatrix() { Grade = 3, BonusFactor = 4 },
new BonusMatrix() { Grade = 4, BonusFactor = 3.5 },
new BonusMatrix() { Grade = 5, BonusFactor = 3 }
};

ICollection<Employee> employees = new List<Employee> {
new Employee() { Id = "01", Name = "A", Grade = 5, Salary = 20000}, //bonus = 20000 * 7 = 60000
new Employee() { Id = "02", Name = "B", Grade = 5, Salary = 18000}, //bonus = 18000 * 7 = 54000
new Employee() { Id = "03", Name = "C", Grade = 4, Salary = 12000}, //bonus = 12000 * 5.5 = 42000
new Employee() { Id = "04", Name = "D", Grade = 4, Salary = 10000}, //bonus = 10000 * 5.5 = 35000
new Employee() { Id = "05", Name = "E", Grade = 3, Salary = 8500}, //bonus = 8500 * 4 = 34000
new Employee() { Id = "06", Name = "F", Grade = 3, Salary = 8000}, //bonus = 8000 * 4 = 32000
new Employee() { Id = "07", Name = "G", Grade = 2, Salary = 5000}, //bonus = 5000 * 3.5 = 27500
new Employee() { Id = "08", Name = "H", Grade = 2, Salary = 4750}, //bonus = 4750 * 3.5 = 26125
new Employee() { Id = "09", Name = "I", Grade = 1, Salary = 3500}, //bonus = 3500 * 3 = 24500
new Employee() { Id = "10", Name = "J", Grade = 1, Salary = 3250} //bonus = 3250 * 3 = 22750
};

var interpreter = new Interpreter(InterpreterOptions.LambdaExpressions | InterpreterOptions.Default);
interpreter.SetVariable(nameof(annualBonus), annualBonus);
interpreter.SetVariable(nameof(employees), employees);

var totalBonus = employees.Sum(x => x.Salary * (annualBonus.SingleOrDefault(y => y.Grade == x.Grade).BonusFactor)); //total = 357875

var evalSum = interpreter.Eval("employees.Sum(x => x.Salary * (annualBonus.SingleOrDefault(y => y.Grade == x.Grade).BonusFactor))");
Assert.AreEqual(totalBonus, evalSum);
}

public class Employee
{
public string Id { get; set; }
public string Name { get; set; }
public int Grade { get; set; }
public double Salary { get; set; }
}

public class BonusMatrix
{
public int Grade { get; set; }
public double BonusFactor { get; set; }
}
}

internal static class GithubIssuesTestExtensionsMethods
Expand Down
Loading

0 comments on commit 7db3e48

Please sign in to comment.