Skip to content

Commit 1ffcd9b

Browse files
authored
Merge pull request #2 from glopesdev/update-dynamic-linq
Update backend to System.Linq.Dynamic.Core
2 parents 6b3d2ef + 2b28d4c commit 1ffcd9b

12 files changed

+287
-6
lines changed

Bonsai.Scripting.Expressions.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bonsai.Scripting.Expression
77
EndProject
88
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bonsai.Scripting.Expressions.Design", "src\Bonsai.Scripting.Expressions.Design\Bonsai.Scripting.Expressions.Design.csproj", "{A6ADFFAA-CA25-545C-AC3F-4BC3D819B8CB}"
99
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bonsai.Scripting.Expressions.Tests", "src\Bonsai.Scripting.Expressions.Tests\Bonsai.Scripting.Expressions.Tests.csproj", "{CE4BD71F-2CCD-4ED5-8F4E-EAC123EEF518}"
11+
EndProject
1012
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{DEE5DD87-39C1-BF34-B639-A387DCCF972B}"
1113
ProjectSection(SolutionItems) = preProject
1214
build\Common.csproj.props = build\Common.csproj.props
@@ -31,6 +33,10 @@ Global
3133
{A6ADFFAA-CA25-545C-AC3F-4BC3D819B8CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
3234
{A6ADFFAA-CA25-545C-AC3F-4BC3D819B8CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
3335
{A6ADFFAA-CA25-545C-AC3F-4BC3D819B8CB}.Release|Any CPU.Build.0 = Release|Any CPU
36+
{CE4BD71F-2CCD-4ED5-8F4E-EAC123EEF518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37+
{CE4BD71F-2CCD-4ED5-8F4E-EAC123EEF518}.Debug|Any CPU.Build.0 = Debug|Any CPU
38+
{CE4BD71F-2CCD-4ED5-8F4E-EAC123EEF518}.Release|Any CPU.ActiveCfg = Release|Any CPU
39+
{CE4BD71F-2CCD-4ED5-8F4E-EAC123EEF518}.Release|Any CPU.Build.0 = Release|Any CPU
3440
EndGlobalSection
3541
GlobalSection(SolutionProperties) = preSolution
3642
HideSolutionNode = FALSE

src/Bonsai.Scripting.Expressions.Design/Bonsai.Scripting.Expressions.Design.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<Description>This package provides editors for expression scripting in the Bonsai programming language.</Description>
44
<PackageTags>$(PackageTags) Design</PackageTags>
55
<UseWindowsForms>true</UseWindowsForms>
6-
<TargetFramework>net472</TargetFramework>
6+
<TargetFrameworks>net472;net8.0-windows</TargetFrameworks>
77
</PropertyGroup>
88
<ItemGroup>
99
<PackageReference Include="Bonsai.Design" Version="2.9.0" />
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net472;net8.0</TargetFrameworks>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
9+
<PackageReference Include="MSTest.TestAdapter" Version="3.8.3" />
10+
<PackageReference Include="MSTest.TestFramework" Version="3.8.3" />
11+
</ItemGroup>
12+
13+
<ItemGroup>
14+
<ProjectReference Include="..\Bonsai.Scripting.Expressions\Bonsai.Scripting.Expressions.csproj" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
19+
</ItemGroup>
20+
21+
</Project>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using Bonsai.Expressions;
2+
using System;
3+
using System.Collections;
4+
using System.Reactive.Linq;
5+
using System.Threading.Tasks;
6+
7+
namespace Bonsai.Scripting.Expressions.Tests
8+
{
9+
[TestClass]
10+
public sealed class ExpressionScriptingTests
11+
{
12+
async Task AssertExpressionTransform<TSource, TResult>(string expression, TSource value, TResult expected)
13+
{
14+
var workflowBuilder = new WorkflowBuilder();
15+
var source = workflowBuilder.Workflow.Add(new CombinatorBuilder { Combinator = new Return<TSource>(value) });
16+
var transform = workflowBuilder.Workflow.Add(new ExpressionTransform { Expression = expression });
17+
var output = workflowBuilder.Workflow.Add(new WorkflowOutputBuilder());
18+
workflowBuilder.Workflow.AddEdge(source, transform, new());
19+
workflowBuilder.Workflow.AddEdge(transform, output, new());
20+
var result = await workflowBuilder.Workflow.BuildObservable<TResult>();
21+
if (expected is ICollection expectedCollection && result is ICollection resultCollection)
22+
CollectionAssert.AreEqual(expectedCollection, resultCollection);
23+
else
24+
Assert.AreEqual(expected, result);
25+
}
26+
27+
[DataTestMethod]
28+
[DataRow("it", 42, 42)]
29+
[DataRow("it * 2", 21, 42)]
30+
[DataRow("Single(it)", 42, 42f)]
31+
[DataRow("Math.PI", 42, Math.PI)]
32+
[DataRow("Convert.ToInt16(it)", 42, (short)42)]
33+
[DataRow("new(it as Data).Data", 42, 42)]
34+
// modern Dynamic LINQ parser
35+
[DataRow("float(it)", 42, 42f)]
36+
[DataRow("long?(it).HasValue", 42, true)]
37+
[DataRow("bool.TrueString", 42, "True")]
38+
[DataRow("new[] { it }", 42, new[] { 42 })]
39+
[DataRow("new[] { it }.Select(x => x * 2).ToArray()", 21, new[] { 42 })]
40+
[DataRow("np(string(null).Length) ?? it", 42, 42)]
41+
public Task TestExpressionTransform<TSource, TResult>(string expression, TSource value, TResult expected)
42+
{
43+
return AssertExpressionTransform(expression, value, expected);
44+
}
45+
46+
[TestMethod]
47+
public Task TestNullExpression() => AssertExpressionTransform("null", 0, (object)null);
48+
49+
[TestMethod]
50+
public Task TestNullString() => AssertExpressionTransform("string(null)", 0, (string)null);
51+
52+
[TestMethod]
53+
public Task TestObjectExpression() => AssertExpressionTransform("object(it)", 42, (object)42);
54+
55+
[DataTestMethod]
56+
[DataRow("single(it)", 42, 42f)]
57+
[DataRow("int64?(it).hasvalue", 42, true)]
58+
[DataRow("math.pi", 42, Math.PI)]
59+
[DataRow("boolean.truestring", 42, "True")]
60+
[DataRow("convert.toint16(it)", 42, (short)42)]
61+
[DataRow("datetime.minvalue.second", 42, 0)]
62+
[DataRow("datetimeoffset.minvalue.second", 42, 0)]
63+
[DataRow("guid.empty.tobytearray()[0]", 42, 0)]
64+
[DataRow("timespan.tickspermillisecond", 0, TimeSpan.TicksPerMillisecond)]
65+
[DataRow("it > 0 ? convert.toint16(it) : int16.minvalue", 42, (short)42)]
66+
public Task TestCasingCompatibility<TSource, TResult>(string expression, TSource value, TResult expected)
67+
{
68+
return AssertExpressionTransform(expression, value, expected);
69+
}
70+
71+
[DataTestMethod]
72+
[DataRow("")]
73+
[DataRow("string(it)")]
74+
public Task TestInvalidExpression(string expression)
75+
{
76+
return Assert.ThrowsExactlyAsync<WorkflowBuildException>(() =>
77+
AssertExpressionTransform(expression, 42, (object)null));
78+
}
79+
80+
class Return<TValue>(TValue value) : Source<TValue>
81+
{
82+
public TValue Value { get; } = value;
83+
84+
public override IObservable<TValue> Generate() => Observable.Return(Value);
85+
}
86+
}
87+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<Description>This package provides operators implementing expression scripting infrastructure.</Description>
4-
<TargetFramework>net472</TargetFramework>
4+
<TargetFrameworks>net472;net8.0</TargetFrameworks>
55
</PropertyGroup>
66
<ItemGroup>
77
<PackageReference Include="Bonsai.Core" Version="2.9.0" />
8-
<PackageReference Include="System.Linq.Dynamic" Version="1.0.7" />
8+
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.7" />
99
</ItemGroup>
1010
</Project>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Collections.Generic;
2+
using System.Linq.Dynamic.Core;
3+
using System.Linq.Dynamic.Core.Tokenizer;
4+
using System.Text;
5+
6+
namespace Bonsai.Scripting.Expressions
7+
{
8+
internal static class CompatibilityAnalyzer
9+
{
10+
internal static readonly Dictionary<string, string> LegacyKeywords = new()
11+
{
12+
{ "boolean", "bool" },
13+
{ "datetime", "DateTime" },
14+
{ "datetimeoffset", "DateTimeOffset" },
15+
{ "guid", "Guid" },
16+
{ "int16", "short" },
17+
{ "int32", "int" },
18+
{ "int64", "long" },
19+
{ "single", "float" },
20+
{ "timespan", "TimeSpan" },
21+
{ "uint32", "uint" },
22+
{ "uint64", "ulong" },
23+
{ "uint16", "ushort" },
24+
{ "math", "Math" },
25+
{ "convert", "Convert" }
26+
};
27+
28+
public static bool ReplaceLegacyKeywords(ParsingConfig? parsingConfig, string text, out string result)
29+
{
30+
result = text;
31+
if (string.IsNullOrEmpty(text))
32+
return false;
33+
34+
35+
List<(Token, string)> replacements = null;
36+
var previousTokenId = TokenId.Unknown;
37+
var textParser = new TextParser(parsingConfig, text);
38+
while (textParser.CurrentToken.Id != TokenId.End)
39+
{
40+
if (textParser.CurrentToken.Id == TokenId.Identifier &&
41+
previousTokenId != TokenId.Dot &&
42+
LegacyKeywords.TryGetValue(textParser.CurrentToken.Text, out var keyword))
43+
{
44+
replacements ??= new();
45+
replacements.Add((textParser.CurrentToken, keyword));
46+
}
47+
48+
previousTokenId = textParser.CurrentToken.Id;
49+
textParser.NextToken();
50+
}
51+
52+
if (replacements?.Count > 0)
53+
{
54+
var sb = new StringBuilder(text);
55+
for (int i = 0; i < replacements.Count; i++)
56+
{
57+
var (token, keyword) = replacements[i];
58+
sb.Remove(token.Pos, token.Text.Length);
59+
sb.Insert(token.Pos, keyword);
60+
}
61+
62+
result = sb.ToString();
63+
return true;
64+
}
65+
66+
return false;
67+
}
68+
}
69+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Linq.Dynamic.Core;
3+
using System.Linq.Dynamic.Core.Exceptions;
4+
using System.Linq.Expressions;
5+
6+
namespace Bonsai.Scripting.Expressions
7+
{
8+
internal class DynamicExpressionHelper
9+
{
10+
public static LambdaExpression ParseLambda(Type delegateType, ParsingConfig? parsingConfig, ParameterExpression[] parameters, Type? resultType, string expression, params object?[] values)
11+
{
12+
return ParseLambda(delegateType, parsingConfig, true, parameters, resultType, expression, values);
13+
}
14+
15+
public static LambdaExpression ParseLambda(ParsingConfig? parsingConfig, Type itType, Type? resultType, string expression, params object?[] values)
16+
{
17+
return ParseLambda(null, parsingConfig, true, new[] { Expression.Parameter(itType, "it") }, resultType, expression, values);
18+
}
19+
20+
public static LambdaExpression ParseLambda(Type? delegateType, ParsingConfig? parsingConfig, bool createParameterCtor, ParameterExpression[] parameters, Type? resultType, string expression, params object?[] values)
21+
{
22+
try
23+
{
24+
return DynamicExpressionParser.ParseLambda(delegateType, parsingConfig, createParameterCtor, parameters, resultType, expression, values);
25+
}
26+
catch (ParseException)
27+
{
28+
if (!CompatibilityAnalyzer.ReplaceLegacyKeywords(parsingConfig, expression, out expression))
29+
throw;
30+
31+
return DynamicExpressionParser.ParseLambda(delegateType, parsingConfig, createParameterCtor, parameters, resultType, expression, values);
32+
}
33+
}
34+
}
35+
}

src/Bonsai.Scripting.Expressions/ExpressionCondition.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Generic;
44
using System.ComponentModel;
55
using System.Linq;
6+
using System.Linq.Dynamic.Core;
67
using System.Linq.Expressions;
78
using System.Reactive.Linq;
89
using System.Reflection;
@@ -52,7 +53,8 @@ public override Expression Build(IEnumerable<Expression> arguments)
5253
{
5354
var source = arguments.First();
5455
var sourceType = source.Type.GetGenericArguments()[0];
55-
var predicate = System.Linq.Dynamic.DynamicExpression.ParseLambda(sourceType, typeof(bool), Expression);
56+
var config = ParsingConfigHelper.CreateParsingConfig(sourceType);
57+
var predicate = DynamicExpressionHelper.ParseLambda(config, sourceType, typeof(bool), Expression);
5658
return System.Linq.Expressions.Expression.Call(whereMethod.MakeGenericMethod(sourceType), source, predicate);
5759
}
5860

src/Bonsai.Scripting.Expressions/ExpressionSink.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Generic;
44
using System.ComponentModel;
55
using System.Linq;
6+
using System.Linq.Dynamic.Core;
67
using System.Linq.Expressions;
78
using System.Reactive.Linq;
89
using System.Reflection;
@@ -59,7 +60,8 @@ public override Expression Build(IEnumerable<Expression> arguments)
5960
var sourceType = source.Type.GetGenericArguments()[0];
6061
var actionType = System.Linq.Expressions.Expression.GetActionType(sourceType);
6162
var itParameter = new[] { System.Linq.Expressions.Expression.Parameter(sourceType, string.Empty) };
62-
var onNext = System.Linq.Dynamic.DynamicExpression.ParseLambda(actionType, itParameter, null, Expression);
63+
var config = ParsingConfigHelper.CreateParsingConfig(sourceType);
64+
var onNext = DynamicExpressionHelper.ParseLambda(actionType, config, itParameter, null, Expression);
6365
return System.Linq.Expressions.Expression.Call(doMethod.MakeGenericMethod(sourceType), source, onNext);
6466
}
6567
else return source;

0 commit comments

Comments
 (0)