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.
  • Loading branch information
Holden Mai authored and Holden Mai committed Sep 19, 2022
1 parent f0cbd31 commit 2b33655
Show file tree
Hide file tree
Showing 3 changed files with 335 additions and 3 deletions.
9 changes: 6 additions & 3 deletions src/DynamicExpresso.Core/Parsing/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3255,11 +3255,14 @@ public InterpreterExpression(ParserArguments parserArguments, string expressionT
_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));
}

// 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
243 changes: 243 additions & 0 deletions test/DynamicExpresso.UnitTest/LambdaExpressionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace DynamicExpresso.UnitTest
{
Expand Down Expand Up @@ -282,6 +283,24 @@ public void Lambda_with_parameter()
Assert.AreEqual(new[] { 4, 5 }, listInt);
}

[Test]
public void Lambda_with_parameter_AsCompiledLambda()
{
var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions);
var parm = new Parameter("x", 1);
var list = new Parameter("list", new[] { 1, 2, 3 });
var listLamba = target.Parse("list.Where(n => n > x)", list, parm).Compile<Func<int[], int, IEnumerable<int>>>();
var result = listLamba(list.Value as int[], 2);
Assert.AreEqual(new[] { 3 }, result);

var listInt = listLamba(list.Value as int[], 1);
Assert.AreEqual(new[] { 2, 3 }, listInt);

// ensure the parameters can be reused with different values
listInt = target.Eval<IEnumerable<int>>("list.Where(n => n > x)", new Parameter("list", new[] { 2, 4, 5 }), new Parameter("x", 2));
Assert.AreEqual(new[] { 4, 5 }, listInt);
}

[Test]
public void Lambda_with_parameter_2()
{
Expand All @@ -300,6 +319,230 @@ public void Lambda_with_variable()
var listInt = target.Eval<IEnumerable<int>>("list.Where(n => n > x)");
Assert.AreEqual(new[] { 2, 3 }, listInt);
}

public class NestedLambdaTestClass
{
public NestedLambdaTestClass()
{
}

public List<NestedLambdaTestClass> Children
{
get; set;
}

public string Name
{
get; set;
}

// TODO
// Add support for non generics with our lambda evaluation
// The below fails to compile
// public string GetChildrenIdentifiers<T>(Func<NestedLambdaTestClass, T> f)
public string GetChildrenIdentifiers<T>(Func<NestedLambdaTestClass, T> f)
{
if (Children == null)
{
return string.Empty;
}
return string.Join(",", Children.Select(f));
}
}

[Test]
public void Lambda_WithMultipleNestedExpressions()
{
NestedLambdaTestClass root = new NestedLambdaTestClass()
{
Name = "Root",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "A",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "B",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C"
},
new NestedLambdaTestClass()
{
Name = "F"
}
}
},
new NestedLambdaTestClass()
{
Name = "F",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C"
},
new NestedLambdaTestClass()
{
Name = "F"
}
}
}
}
},
new NestedLambdaTestClass()
{
Name = "D",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "E",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C"
},
new NestedLambdaTestClass()
{
Name = "F"
}
}
},
new NestedLambdaTestClass()
{
Name = "G",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C"
},
new NestedLambdaTestClass()
{
Name = "F"
}
}
}
}
}
}
},
new NestedLambdaTestClass()
{
Name = "B",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "B",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C"
},
new NestedLambdaTestClass()
{
Name = "F"
}
}
},
new NestedLambdaTestClass()
{
Name = "F",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C"
},
new NestedLambdaTestClass()
{
Name = "F"
}
}
}
}
},
new NestedLambdaTestClass()
{
Name = "D",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "E",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C"
},
new NestedLambdaTestClass()
{
Name = "F",
Children = new List<NestedLambdaTestClass>()
{
new NestedLambdaTestClass()
{
Name = "C"
},
new NestedLambdaTestClass()
{
Name = "F"
}
}
}
}
},
new NestedLambdaTestClass()
{
Name = "G"
}
}
}
}
}
}
};
var expectedResult = root.GetChildrenIdentifiers(
// root
l1 => l1.Name + l1.GetChildrenIdentifiers(
// level 2, references my parameter, plus original lamda
l2 => l2.Name + l1.Name + l2.GetChildrenIdentifiers(
// level 3, references my parameter, plus parameter from l1 lamda
l3 => l3.Name + l2.Name + l3.GetChildrenIdentifiers(
// level 4, references my parameter, plus all parameters that have been used
l4 => l4.Name + l2.Name + l3.Name + l1.Name + root.Name)
)));

var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions);

var evalResult = target.Eval<string>(@"root.GetChildrenIdentifiers(
l1 => l1.Name + l1.GetChildrenIdentifiers(
l2 => l2.Name + l1.Name + l2.GetChildrenIdentifiers(
l3 => l3.Name + l2.Name + l3.GetChildrenIdentifiers(
l4 => l4.Name + l2.Name + l3.Name + l1.Name + root.Name)
)))", new Parameter(nameof(root), root));
Assert.AreEqual(expectedResult, evalResult);
}
}

/// <summary>
Expand Down

0 comments on commit 2b33655

Please sign in to comment.