Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Microsoft.AdaptiveExpressions.Core, fork of AdaptiveExpressions on System.Text.Json (and AOT compatible) #6783

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions Microsoft.Bot.Builder.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29123.88
# Visual Studio Version 17
VisualStudioVersion = 17.9.34728.123
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{4269F3C3-6B42-419B-B64A-3E6DC0F1574A}"
ProjectSection(SolutionItems) = preProject
Expand Down Expand Up @@ -111,8 +111,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
bot.png = bot.png
bot_icon.png = bot_icon.png
BotBuilder-DotNet.ruleset = BotBuilder-DotNet.ruleset
bot_icon.png = bot_icon.png
CodeCoverage.runsettings = CodeCoverage.runsettings
libraries\copySchemas.cmd = libraries\copySchemas.cmd
Directory.Build.props = Directory.Build.props
Expand Down Expand Up @@ -233,7 +233,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Str
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Streaming.Tests.Client", "tests\Microsoft.Bot.Connector.Streaming.Tests.Client\Microsoft.Bot.Connector.Streaming.Tests.Client.csproj", "{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Bot.Connector.Streaming.Perf", "tests\Microsoft.Bot.Connector.Streaming.Perf\Microsoft.Bot.Connector.Streaming.Perf.csproj", "{B49A3201-5BEE-426C-A082-D92D52172E06}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Connector.Streaming.Perf", "tests\Microsoft.Bot.Connector.Streaming.Perf\Microsoft.Bot.Connector.Streaming.Perf.csproj", "{B49A3201-5BEE-426C-A082-D92D52172E06}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveExpressionsSTJ", "libraries\AdaptiveExpressionsSTJ\AdaptiveExpressionsSTJ.csproj", "{0D974CFD-0E69-448F-B375-BBDA457FD975}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveExpressionsSTJ.Tests", "tests\AdaptiveExpressionsSTJ.Tests\AdaptiveExpressionsSTJ.Tests.csproj", "{F90C5110-0FF5-4775-9750-CF4E73B8AB32}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveExpressionsSTJ.AOT.Tests", "tests\AdaptiveExpressionsSTJ.AOT.Tests\AdaptiveExpressionsSTJ.AOT.Tests.csproj", "{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -923,6 +929,30 @@ Global
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release|Any CPU.Build.0 = Release|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{B49A3201-5BEE-426C-A082-D92D52172E06}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Release|Any CPU.Build.0 = Release|Any CPU
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{0D974CFD-0E69-448F-B375-BBDA457FD975}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Release|Any CPU.Build.0 = Release|Any CPU
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{F90C5110-0FF5-4775-9750-CF4E73B8AB32}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Release|Any CPU.Build.0 = Release|Any CPU
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580}.Release-Windows|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1026,6 +1056,9 @@ Global
{FB7ADCDF-C0A5-49EA-8ADC-CC77B6FB9D71} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
{2E5AD07C-4F6E-4B6B-BEFE-9FBE9F789161} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
{B49A3201-5BEE-426C-A082-D92D52172E06} = {EBFEF03F-9ACE-4312-89D7-2C8A147CDF9C}
{0D974CFD-0E69-448F-B375-BBDA457FD975} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
{F90C5110-0FF5-4775-9750-CF4E73B8AB32} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
{DD3C8FAE-CF52-4102-98C8-B2B88A4B0580} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}
Expand Down
55 changes: 55 additions & 0 deletions libraries/AdaptiveExpressionsSTJ/AdaptiveExpressionsSTJ.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net6.0;net8.0</TargetFrameworks>
<Version Condition=" '$(ReleasePackageVersion)' == '' ">$(LocalPackageVersion)</Version>
<Version Condition=" '$(ReleasePackageVersion)' != '' ">$(ReleasePackageVersion)</Version>
<PackageVersion Condition=" '$(ReleasePackageVersion)' == '' ">$(LocalPackageVersion)</PackageVersion>
<PackageVersion Condition=" '$(ReleasePackageVersion)' != '' ">$(ReleasePackageVersion)</PackageVersion>
<Configurations>Debug;Release</Configurations>
<DocumentationFile>bin\$(Configuration)\netstandard2.0\AdaptiveExpressions.xml</DocumentationFile>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>
<PolySharpIncludeRuntimeSupportedAttributes>true</PolySharpIncludeRuntimeSupportedAttributes>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>Full</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>

<PropertyGroup>
<!--<TargetFramework>netstandard2.0</TargetFramework>-->
<PackageId>AdaptiveExpressionsSTJ</PackageId>
<Description>This library implements Microsoft Bot Builder Expression evaluator for .NET</Description>
<Summary>This library implements Microsoft Bot Builder Expression evaluator for .NET</Summary>
</PropertyGroup>

<ItemGroup>
<None Remove="WindowsIanaMapping" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="WindowsIanaMapping" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Antlr4.Runtime.Standard" Version="4.11.1" />
<PackageReference Include="JsonPath.Net" Version="1.0.1.2" />
<PackageReference Include="JsonSchema.Net" Version="7.0.0.3" />
<PackageReference Include="Microsoft.CSharp" Version="4.5.0" />
<PackageReference Include="Microsoft.Recognizers.Text.DataTypes.TimexExpression" Version="1.3.2" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Text.Json" Version="8.0.3" />
</ItemGroup>

<ItemGroup>
<Folder Include="Generated\" />
</ItemGroup>

<ItemGroup>
<None Include="icon.png" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
using AdaptiveExpressions.Properties;
using Microsoft.Recognizers.Text.DataTypes.TimexExpression;

namespace AdaptiveExpressions
{
/// <summary>
/// Json serializer context for all AdaptiveExpressions types.
/// </summary>
[JsonSerializable(typeof(IntExpression))]
[JsonSerializable(typeof(TimexProperty))]
[JsonSerializable(typeof(DateTime))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(double))]
internal partial class AdaptiveExpressionsSerializerContext : JsonSerializerContext
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## AdaptiveExpressions on System.Text.Json overview

To make AdaptiveExpressions library work with [AOT compilation](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=net7%2Cwindows), it was necessary to migrate from Newtonsoft to System.Text.Json. This has user-visible API and implementation differences.

### Expanded IMemory interface and constrained expression return types

The expression system calls out to the IMemory object to get and set values into a user-provided object graph. It is expected that TryGetValue returns only primitive types like number, string. But TryGetValue can also return lists or other objects. In many cases those objects are opaque and are just passed back into IMemory.SetValue or returned back to the caller, but in other cases the expression engine used to try to manipulate the objects or serialize them to json. Because all such methods would require reflection, those responsibilities are now delegated back to IMemory and the expression engine only understands primitive types, a small number of List types (`IList` and `List<object>`), and System.Text.Json types (e.g. `JsonArray` and `JsonObject`).

In some cases the expression engine will create a list (e.g. if you foreach over an object), and in this case the expression will create a `List<object>` and the user-implemented IMemory is expected to handle this externally-created object as well.

### Added JsonNodeMemory

For callers that don't want to implement IMemory, the easiest way to migrate to AOT is to use JsonObjects to store the data you want to evaluate expressions against and wrap them in JsonNodeMemory to pass in to evaluation functions. You can also use JsonSerializer.SerializeToNode to serialize an existing non-json object into JsonNode in an AOT-compatible way.

### Many methods now take JsonTypeInfo

System.Text.Json supports AOT by not relying on reflection, and instead the link between a type and its converter is via `JsonTypeInfo`. You get one of these using the [STJ source generator](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation?pivots=dotnet-8-0).

Implementation within and APIs on AdaptiveExpressions that would have needed to do json serialization on unknown types now are marked as `[RequiresDynamicCode]` and `[RequiresUnreferencedCode]` and there are overloads of those methods and types that take JsonTypeInfo which can be used instead.

### Testing AOT mode

It's difficult to publish & test component in AOT compilation mode, so for testing components in AOT mode we turn on AOT warnings and ensure that the tests and product code are AOT-warning free to convince ourselves that the component will behave correctly when compiled as AOT with trimming on.

AdaptiveExpressionsSTJ.Tests are using some of the AOT patterns but still test SimpleObjectMemory paths and other non-AOT compatible paths. AdaptiveExpressionsSTJ.AOT.Tests is a copy of most of the tests from AdaptiveExpressionsSTJ.Tests but rewritten to be going through exclusively AOT-compatible modes. Functionally this means only testing against the JsonNodeMemory backing implementation and thus some of the round-trip tests aren't particularly interesting but they remain to show others how to convert such code in the future.
28 changes: 28 additions & 0 deletions libraries/AdaptiveExpressionsSTJ/BuiltinFunctions/Abs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Globalization;

namespace AdaptiveExpressions.BuiltinFunctions
{
/// <summary>
/// Returns the absolute value of the specified number.
/// </summary>
internal class Abs : NumberTransformEvaluator
{
/// <summary>
/// Initializes a new instance of the <see cref="Abs"/> class.
/// </summary>
public Abs()
: base(ExpressionType.Abs, Function)
{
}

private static object Function(IReadOnlyList<object> args)
{
return Math.Abs(Convert.ToDouble(args[0], CultureInfo.InvariantCulture));
}
}
}
70 changes: 70 additions & 0 deletions libraries/AdaptiveExpressionsSTJ/BuiltinFunctions/Accessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using AdaptiveExpressions.Memory;

namespace AdaptiveExpressions.BuiltinFunctions
{
/// <summary>
/// Used to access the variable value corresponding to the path.
/// </summary>
internal class Accessor : ExpressionEvaluator
{
/// <summary>
/// Initializes a new instance of the <see cref="Accessor"/> class.
/// </summary>
public Accessor()
: base(ExpressionType.Accessor, Evaluator, ReturnType.Object, Validator)
{
}

private static (object value, string error) Evaluator(Expression expression, IMemory state, Options options)
{
var (path, left, error) = FunctionUtils.TryAccumulatePath(expression, state, options);

if (error != null)
{
return (null, error);
}

if (left == null)
{
// fully converted to path, so we just delegate to memory scope
return FunctionUtils.WrapGetValue(state, path, options);
}
else
{
// stop at somewhere, so we figure out what's left
var (newScope, err) = left.TryEvaluate(state, options);
if (err != null)
{
return (null, err);
}

return FunctionUtils.WrapGetValue(state.CreateMemoryFrom(newScope), path, options);
}
}

private static void Validator(Expression expression)
{
var children = expression.Children;
if (children.Length == 0
|| !(children[0] is Constant cnst)
|| cnst.ReturnType != ReturnType.String)
{
throw new ArgumentException($"{expression} must have a string as first argument.");
}

if (children.Length > 2)
{
throw new ArgumentException($"{expression} has more than 2 children.");
}

if (children.Length == 2 && (children[1].ReturnType & ReturnType.Object) == 0)
{
throw new ArgumentException($"{expression} must have an object as its second argument.");
}
}
}
}
79 changes: 79 additions & 0 deletions libraries/AdaptiveExpressionsSTJ/BuiltinFunctions/Add.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Globalization;

namespace AdaptiveExpressions.BuiltinFunctions
{
/// <summary>
/// Return the result from adding two or more numbers (pure number case) or concatting two or more strings (other case).
/// </summary>
internal class Add : ExpressionEvaluator
{
/// <summary>
/// Initializes a new instance of the <see cref="Add"/> class.
/// </summary>
public Add()
: base(ExpressionType.Add, Evaluator(), ReturnType.String | ReturnType.Number, Validator)
{
}

private static EvaluateExpressionDelegate Evaluator()
{
return FunctionUtils.ApplySequenceWithError(
args =>
{
object result = null;
string error = null;
var firstItem = args[0];
var secondItem = args[1];
var stringConcat = !firstItem.IsNumber() || !secondItem.IsNumber();

if ((firstItem == null && secondItem.IsNumber())
|| (secondItem == null && firstItem.IsNumber()))
{
error = "Operator '+' or add cannot be applied to operands of type 'number' and null object.";
}
else
{
if (stringConcat)
{
result = $"{firstItem?.ToString()}{secondItem?.ToString()}";
}
else
{
result = EvalAdd(args[0], args[1]);
}
}

return (result, error);
}, FunctionUtils.VerifyNumberOrStringOrNull);
}

private static object EvalAdd(object a, object b)
{
if (a == null)
{
throw new ArgumentNullException(nameof(a));
}

if (b == null)
{
throw new ArgumentNullException(nameof(b));
}

if (a.IsInteger() && b.IsInteger())
{
return Convert.ToInt64(a, CultureInfo.InvariantCulture) + Convert.ToInt64(b, CultureInfo.InvariantCulture);
}

return FunctionUtils.CultureInvariantDoubleConvert(a) + FunctionUtils.CultureInvariantDoubleConvert(b);
}

private static void Validator(Expression expression)
{
FunctionUtils.ValidateArityAndAnyType(expression, 2, int.MaxValue, ReturnType.String | ReturnType.Number);
}
}
}
29 changes: 29 additions & 0 deletions libraries/AdaptiveExpressionsSTJ/BuiltinFunctions/AddDays.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace AdaptiveExpressions.BuiltinFunctions
{
/// <summary>
/// Add a number of days to a timestamp.
/// AddDays function takes a timestamp string, an interval integer,
/// an optional format string whose default value "yyyy-MM-ddTHH:mm:ss.fffZ"
/// and an optional locale string whose default value is Thread.CurrentThread.CurrentCulture.Name.
/// </summary>
internal class AddDays : TimeTransformEvaluator
{
/// <summary>
/// Initializes a new instance of the <see cref="AddDays"/> class.
/// </summary>
public AddDays()
: base(ExpressionType.AddDays, Function)
{
}

private static DateTime Function(DateTime time, int interval)
{
return time.AddDays(interval);
}
}
}
Loading
Loading