From c2259361238449e450e0a4d0a8aee82822b750e3 Mon Sep 17 00:00:00 2001 From: David Zhang Date: Sun, 16 Nov 2025 14:56:20 -0800 Subject: [PATCH 1/5] Enhance Lambda class: optimize parameter handling and invocation logic Enhance Eval and Lambda classes: introduce preferInterpretation flag for optimized expression evaluation --- src/DynamicExpresso.Core/Interpreter.cs | 8 +- src/DynamicExpresso.Core/Lambda.cs | 303 +++++++++++++++++++++--- 2 files changed, 277 insertions(+), 34 deletions(-) diff --git a/src/DynamicExpresso.Core/Interpreter.cs b/src/DynamicExpresso.Core/Interpreter.cs index ce84fc0..f7f49f0 100644 --- a/src/DynamicExpresso.Core/Interpreter.cs +++ b/src/DynamicExpresso.Core/Interpreter.cs @@ -523,7 +523,9 @@ public T Eval(string expressionText, params Parameter[] parameters) /// public object Eval(string expressionText, Type expressionType, params Parameter[] parameters) { - return Parse(expressionText, expressionType, parameters).Invoke(parameters); + // Eval is intended for one-off expressions: prefer interpretation to avoid IL generation cost. + var lambda = ParseAsLambda(expressionText, expressionType, parameters, preferInterpretation: true); + return lambda.Invoke(parameters); } #endregion @@ -548,7 +550,7 @@ public IdentifiersInfo DetectIdentifiers(string expression, DetectorOptions opti #region Private methods - private Lambda ParseAsLambda(string expressionText, Type expressionType, Parameter[] parameters) + private Lambda ParseAsLambda(string expressionText, Type expressionType, Parameter[] parameters, bool preferInterpretation = false) { var arguments = new ParserArguments( expressionText, @@ -561,7 +563,7 @@ private Lambda ParseAsLambda(string expressionText, Type expressionType, Paramet foreach (var visitor in Visitors) expression = visitor.Visit(expression); - var lambda = new Lambda(expression, arguments); + var lambda = new Lambda(expression, arguments, preferInterpretation); #if TEST_DetectIdentifiers AssertDetectIdentifiers(lambda); diff --git a/src/DynamicExpresso.Core/Lambda.cs b/src/DynamicExpresso.Core/Lambda.cs index 23b8f91..27108ac 100644 --- a/src/DynamicExpresso.Core/Lambda.cs +++ b/src/DynamicExpresso.Core/Lambda.cs @@ -16,16 +16,73 @@ public class Lambda { private readonly Expression _expression; private readonly ParserArguments _parserArguments; + + // Delegate whose parameters are in UsedParameters order. private readonly Lazy _delegate; + private readonly bool _preferInterpretation; + + // Snapshots taken at construction time so we don't re-enumerate and allocate on every call. + private readonly Parameter[] _usedParameters; + private readonly ParameterExpression[] _declaredParameterExpressions; + + // For each used parameter index, which declared parameter index it corresponds to. + private readonly int[] _usedToDeclaredIndex; + private readonly int _declaredCount; + private readonly int _usedCount; + + // Fast path: declared-order object[] -> result. + private readonly Lazy> _fastInvokerFromDeclared; - internal Lambda(Expression expression, ParserArguments parserArguments) + internal Lambda(Expression expression, ParserArguments parserArguments, bool preferInterpretation = false) { _expression = expression ?? throw new ArgumentNullException(nameof(expression)); _parserArguments = parserArguments ?? throw new ArgumentNullException(nameof(parserArguments)); - // Note: I always lazy compile the generic lambda. Maybe in the future this can be a setting because if I generate a typed delegate this compilation is not required. + _preferInterpretation = preferInterpretation; + + // Snapshot parameters once: avoids repeated enumeration/allocation. + var declaredParameters = _parserArguments.DeclaredParameters.ToArray(); + _usedParameters = _parserArguments.UsedParameters.ToArray(); + + _declaredParameterExpressions = declaredParameters.Select(p => p.Expression).ToArray(); + + _declaredCount = declaredParameters.Length; + _usedCount = _usedParameters.Length; + _delegate = new Lazy(() => - Expression.Lambda(_expression, _parserArguments.UsedParameters.Select(p => p.Expression).ToArray()).Compile()); + Expression.Lambda(_expression, _usedParameters.Select(p => p.Expression).ToArray()) + .Compile(_preferInterpretation)); + + // Precompute used-index -> declared-index mapping for the fast path. + if (_usedCount > 0) + { + if (_declaredCount == 0) + throw new InvalidOperationException("Used parameters exist but there are no declared parameters."); + + var nameToDeclaredIndex = + new Dictionary(_declaredCount, _parserArguments.Settings.KeyComparer); + for (var i = 0; i < declaredParameters.Length; i++) + { + nameToDeclaredIndex[declaredParameters[i].Name] = i; + } + + _usedToDeclaredIndex = new int[_usedCount]; + for (var i = 0; i < _usedCount; i++) + { + var usedName = _usedParameters[i].Name; + if (!nameToDeclaredIndex.TryGetValue(usedName, out var declaredIndex)) + throw new InvalidOperationException( + $"Used parameter '{usedName}' was not found in declared parameters."); + + _usedToDeclaredIndex[i] = declaredIndex; + } + } + else + { + _usedToDeclaredIndex = Array.Empty(); + } + + _fastInvokerFromDeclared = new Lazy>(BuildFastInvokerFromDeclared); } public Expression Expression { get { return _expression; } } @@ -45,6 +102,7 @@ internal Lambda(Expression expression, ParserArguments parserArguments) /// /// The used parameters. public IEnumerable UsedParameters { get { return _parserArguments.UsedParameters; } } + /// /// Gets the parameters declared when parsing the expression. /// @@ -56,7 +114,14 @@ internal Lambda(Expression expression, ParserArguments parserArguments) public object Invoke() { - return InvokeWithUsedParameters(new object[0]); + if (_usedCount == 0) + { + return _fastInvokerFromDeclared.Value(Array.Empty()); + } + + // Fallback: preserve the original behavior where missing parameters + // TargetParameterCountException is likely to be thrown. + return InvokeWithUsedParameters(Array.Empty()); } public object Invoke(params Parameter[] parameters) @@ -64,46 +129,97 @@ public object Invoke(params Parameter[] parameters) return Invoke((IEnumerable)parameters); } + /// + /// Invoke the expression with the given named parameters. + /// Parameters are matched by name against the parameters actually used in the expression. + /// public object Invoke(IEnumerable parameters) { - var args = (from usedParameter in UsedParameters - from actualParameter in parameters - where usedParameter.Name.Equals(actualParameter.Name, _parserArguments.Settings.KeyComparison) - select actualParameter.Value) - .ToArray(); + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); - return InvokeWithUsedParameters(args); + var paramList = parameters as IList ?? parameters.ToArray(); + var matchedValues = new List(_usedCount); + + foreach (var used in _usedParameters) + { + foreach (var actual in paramList) + { + if (actual != null && + used.Name.Equals(actual.Name, _parserArguments.Settings.KeyComparison)) + { + matchedValues.Add(actual.Value); + } + } + } + + if (_usedCount == 0) + { + return _fastInvokerFromDeclared.Value(Array.Empty()); + } + + if (matchedValues.Count == _usedCount) + { + var declaredArgs = new object[_declaredCount]; + for (var i = 0; i < _usedCount; i++) + { + var declaredIndex = _usedToDeclaredIndex[i]; + declaredArgs[declaredIndex] = matchedValues[i]; + } + + return Invoke(declaredArgs); + } + + return InvokeWithUsedParameters(matchedValues.ToArray()); } /// - /// Invoke the expression with the given parameters values. + /// Invoke the expression with the given parameter values. + /// The values are in the same order as the parameters declared when parsing (DeclaredParameters). + /// Only the parameters actually used in the expression are passed to the underlying delegate. /// - /// Order of parameters must be the same of the parameters used during parse (DeclaredParameters). - /// + /// Values for declared parameters, in declared order. public object Invoke(params object[] args) { - var parameters = new List(); - var declaredParameters = DeclaredParameters.ToArray(); + if (args == null) + { + return Invoke(); + } + + if (_declaredCount != args.Length) + throw new InvalidOperationException(ErrorMessages.ArgumentCountMismatch); - if (args != null) + // No parameters are actually used: ignore any supplied values. + if (_usedCount == 0) { - if (declaredParameters.Length != args.Length) - throw new InvalidOperationException(ErrorMessages.ArgumentCountMismatch); + return _fastInvokerFromDeclared.Value(Array.Empty()); + } - for (var i = 0; i < args.Length; i++) + // Fast path: all values already directly assignable to the expected parameter types. + if (CanUseFastInvoker(args)) + { + try { - var parameter = new Parameter( - declaredParameters[i].Name, - declaredParameters[i].Type, - args[i]); + return _fastInvokerFromDeclared.Value(args); + } + catch (TargetInvocationException exc) + { + if (exc.InnerException != null) + ExceptionDispatchInfo.Capture(exc.InnerException).Throw(); - parameters.Add(parameter); + throw; } } - return Invoke(parameters); + var usedArgs = BuildUsedArgsFromDeclared(args); + return InvokeWithUsedParameters(usedArgs); } + /// + /// orderedUsedArgs must be in UsedParameters order (the same order used to compile _delegate). + /// This method preserves the original DynamicInvoke-based behavior, including exception types + /// for mismatched argument counts and conversion failures. + /// private object InvokeWithUsedParameters(object[] orderedArgs) { try @@ -119,6 +235,125 @@ private object InvokeWithUsedParameters(object[] orderedArgs) } } + private object[] BuildUsedArgsFromDeclared(object[] declaredArgs) + { + if (_usedCount == 0) + return Array.Empty(); + + var used = new object[_usedCount]; + for (var i = 0; i < _usedCount; i++) + { + var declaredIndex = _usedToDeclaredIndex[i]; + used[i] = declaredArgs[declaredIndex]; + } + + return used; + } + + private bool CanUseFastInvoker(object[] declaredArgs) + { + if (_usedCount == 0) + return true; + + if (declaredArgs == null || declaredArgs.Length != _declaredCount) + return false; + + for (var i = 0; i < _usedCount; i++) + { + var declaredIndex = _usedToDeclaredIndex[i]; + var value = declaredArgs[declaredIndex]; + var targetType = _usedParameters[i].Type; + + if (!IsDirectlyAssignable(value, targetType)) + return false; + } + + return true; + } + + private static bool IsDirectlyAssignable(object value, Type targetType) + { + if (targetType == typeof(object)) + return true; + + var underlying = Nullable.GetUnderlyingType(targetType); + + if (value == null) + { + // null is allowed for reference types and Nullable + return underlying != null || !targetType.IsValueType; + } + + // If it's a Nullable, we allow values of T directly. + var effectiveType = underlying ?? targetType; + return effectiveType.IsInstanceOfType(value); + } + + private Func BuildFastInvokerFromDeclared() + { + // Ensure the underlying delegate is compiled once. + var del = _delegate.Value; + var delType = del.GetType(); + + var argsParam = Expression.Parameter(typeof(object[]), "args"); + + Expression body; + + if (_usedCount == 0) + { + var invokeExpr = Expression.Invoke(Expression.Constant(del, delType)); + if (invokeExpr.Type == typeof(void)) + { + body = Expression.Block(invokeExpr, Expression.Constant(null, typeof(object))); + } + else if (invokeExpr.Type.IsValueType) + { + body = Expression.Convert(invokeExpr, typeof(object)); + } + else + { + body = invokeExpr; + } + } + else + { + var callArgs = new Expression[_usedCount]; + + for (var i = 0; i < _usedCount; i++) + { + var declaredIndex = _usedToDeclaredIndex[i]; + + // args[declaredIndex] + var indexExpr = Expression.Constant(declaredIndex); + var accessExpr = Expression.ArrayIndex(argsParam, indexExpr); + + // We only use this fast path when IsDirectlyAssignable has already confirmed + // that the runtime value is compatible with the target type, so this Convert + // can't introduce new InvalidCastExceptions compared to DynamicInvoke. + var converted = Expression.Convert(accessExpr, _usedParameters[i].Type); + callArgs[i] = converted; + } + + var invokeExpr = Expression.Invoke(Expression.Constant(del, delType), callArgs); + + if (invokeExpr.Type == typeof(void)) + { + body = Expression.Block(invokeExpr, Expression.Constant(null, typeof(object))); + } + else if (invokeExpr.Type.IsValueType) + { + body = Expression.Convert(invokeExpr, typeof(object)); + } + else + { + body = invokeExpr; + } + } + + var lambda = Expression.Lambda>(body, argsParam); + return lambda.Compile(_preferInterpretation); + } + public override string ToString() { return ExpressionText; @@ -127,7 +362,10 @@ public override string ToString() /// /// Generate the given delegate by compiling the lambda expression. /// - /// The delegate to generate. Delegate parameters must match the one defined when creating the expression, see UsedParameters. + /// + /// The delegate to generate. Delegate parameters must match the ones defined + /// when creating the expression, see DeclaredParameters. + /// public TDelegate Compile() { var lambdaExpression = LambdaExpression(); @@ -145,22 +383,25 @@ public TDelegate Compile(IEnumerable parameters) /// Generate a lambda expression. /// /// The lambda expression. - /// The delegate to generate. Delegate parameters must match the one defined when creating the expression, see UsedParameters. + /// + /// The delegate to generate. Delegate parameters must match the ones defined + /// when creating the expression, see DeclaredParameters. + /// public Expression LambdaExpression() { - return Expression.Lambda(_expression, DeclaredParameters.Select(p => p.Expression).ToArray()); + return Expression.Lambda(_expression, _declaredParameterExpressions); } internal LambdaExpression LambdaExpression(Type delegateType) { - var parameterExpressions = DeclaredParameters.Select(p => p.Expression).ToArray(); + var parameterExpressions = _declaredParameterExpressions; var types = delegateType.GetGenericArguments(); // return type - if (delegateType.GetGenericTypeDefinition() == ReflectionExtensions.GetFuncType(parameterExpressions.Length)) + var genericType = delegateType.GetGenericTypeDefinition(); + if (genericType == ReflectionExtensions.GetFuncType(parameterExpressions.Length)) types[types.Length - 1] = _expression.Type; - var genericType = delegateType.GetGenericTypeDefinition(); var inferredDelegateType = genericType.MakeGenericType(types); return Expression.Lambda(inferredDelegateType, _expression, parameterExpressions); } From 98cf10f50ca48dc793e402254e4a1264634fa361 Mon Sep 17 00:00:00 2001 From: David Zhang Date: Sat, 22 Nov 2025 01:24:48 -0800 Subject: [PATCH 2/5] Add BenchmarkDotNet suite for lambda invocation --- .gitignore | 1 + DynamicExpresso.sln | 39 +++++++++ README.md | 6 ++ .../DynamicExpresso.Benchmarks.csproj | 12 +++ .../LambdaBenchmarks.cs | 79 +++++++++++++++++++ .../DynamicExpresso.Benchmarks/Program.cs | 3 + 6 files changed, 140 insertions(+) create mode 100644 benchmark/DynamicExpresso.Benchmarks/DynamicExpresso.Benchmarks.csproj create mode 100644 benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs create mode 100644 benchmark/DynamicExpresso.Benchmarks/Program.cs diff --git a/.gitignore b/.gitignore index 9997611..ec89885 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,7 @@ stylecop.* ~$* *.dbmdl Generated_Code #added for RIA/Silverlight projects +BenchmarkDotNet.Artifacts/ # Backup & report files from converting an old project file to a newer # Visual Studio version. Backup files are not needed, because we have git ;-) diff --git a/DynamicExpresso.sln b/DynamicExpresso.sln index 52825f2..1a4e450 100644 --- a/DynamicExpresso.sln +++ b/DynamicExpresso.sln @@ -12,24 +12,63 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{4088369E-A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicExpresso.Core", "src\DynamicExpresso.Core\DynamicExpresso.Core.csproj", "{C6B7C0D2-B84A-4307-9C61-D95613DB564D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmark", "benchmark", "{09EED85C-BE3C-7566-DC0E-2E8E43466740}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicExpresso.Benchmarks", "benchmark\DynamicExpresso.Benchmarks\DynamicExpresso.Benchmarks.csproj", "{394496C4-B878-4F0C-8471-2417F3205FC8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|x64.ActiveCfg = Debug|Any CPU + {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|x64.Build.0 = Debug|Any CPU + {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|x86.ActiveCfg = Debug|Any CPU + {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Debug|x86.Build.0 = Debug|Any CPU {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|Any CPU.Build.0 = Release|Any CPU + {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|x64.ActiveCfg = Release|Any CPU + {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|x64.Build.0 = Release|Any CPU + {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|x86.ActiveCfg = Release|Any CPU + {33157A92-C6B2-4A51-8262-1FEBFD6558BE}.Release|x86.Build.0 = Release|Any CPU {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|x64.Build.0 = Debug|Any CPU + {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Debug|x86.Build.0 = Debug|Any CPU {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|Any CPU.ActiveCfg = Release|Any CPU {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|Any CPU.Build.0 = Release|Any CPU + {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|x64.ActiveCfg = Release|Any CPU + {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|x64.Build.0 = Release|Any CPU + {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|x86.ActiveCfg = Release|Any CPU + {C6B7C0D2-B84A-4307-9C61-D95613DB564D}.Release|x86.Build.0 = Release|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|x64.ActiveCfg = Debug|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|x64.Build.0 = Debug|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|x86.ActiveCfg = Debug|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Debug|x86.Build.0 = Debug|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|Any CPU.Build.0 = Release|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|x64.ActiveCfg = Release|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|x64.Build.0 = Release|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|x86.ActiveCfg = Release|Any CPU + {394496C4-B878-4F0C-8471-2417F3205FC8}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {394496C4-B878-4F0C-8471-2417F3205FC8} = {09EED85C-BE3C-7566-DC0E-2E8E43466740} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A36C3463-448E-4051-AE87-A2994E36C1EC} EndGlobalSection diff --git a/README.md b/README.md index 1533f6c..ba05cac 100644 --- a/README.md +++ b/README.md @@ -560,6 +560,12 @@ or run unit tests for a specific project with a specific framework: Add `--logger:trx` to generate test results for VSTS. +## Benchmarks + +This repository includes a BenchmarkDotNet project under `benchmark/DynamicExpresso.Benchmarks` to measure interpreter hot-paths. + + dotnet run -c Release --project benchmark/DynamicExpresso.Benchmarks + ## Release notes See [releases page](https://github.com/dynamicexpresso/DynamicExpresso/releases). diff --git a/benchmark/DynamicExpresso.Benchmarks/DynamicExpresso.Benchmarks.csproj b/benchmark/DynamicExpresso.Benchmarks/DynamicExpresso.Benchmarks.csproj new file mode 100644 index 0000000..c2afd82 --- /dev/null +++ b/benchmark/DynamicExpresso.Benchmarks/DynamicExpresso.Benchmarks.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + enable + enable + + + + + + diff --git a/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs b/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs new file mode 100644 index 0000000..2212f18 --- /dev/null +++ b/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs @@ -0,0 +1,79 @@ +using BenchmarkDotNet.Attributes; + +namespace DynamicExpresso.Benchmarks; + +[MemoryDiagnoser] +public class LambdaBenchmarks +{ + private Interpreter _interpreter = null!; + + private Lambda _lambda = null!; + + private object[] _args = null!; + private Parameter[] _declared = null!; + private Parameter[] _parameterValues = null!; + + private const int A = 1; + private const int B = 2; + private const int C = 3; + private const int D = 4; + private const int E = 5; + private const int F = 6; + + [GlobalSetup] + public void Setup() + { + _interpreter = new Interpreter(); + + _declared = new[] + { + new Parameter("a", typeof(int)), + new Parameter("b", typeof(int)), + new Parameter("c", typeof(int)), + new Parameter("d", typeof(int)), + new Parameter("e", typeof(int)), + new Parameter("f", typeof(int)) + }; + + _lambda = _interpreter.Parse( + "((a + b) * c - (double)d / (e + f + 1)) + Math.Max(a, b)", + typeof(double), + _declared); + + _args = new object[] { A, B, C, D, E, F }; + + _parameterValues = new[] + { + new Parameter("a", typeof(int), A), + new Parameter("b", typeof(int), B), + new Parameter("c", typeof(int), C), + new Parameter("d", typeof(int), D), + new Parameter("e", typeof(int), E), + new Parameter("f", typeof(int), F) + }; + } + + [Benchmark(Description = "Invoke cached lambda (object[])")] + public double Invoke_ObjectArray() + { + double sum = 0; + for (var i = 0; i < 100_000; i++) + { + sum += (double)_lambda.Invoke(_args); + } + + return sum; + } + + [Benchmark(Description = "Invoke cached lambda (IEnumerable)")] + public double Invoke_ParametersEnumerable() + { + double sum = 0; + for (var i = 0; i < 100_000; i++) + { + sum += (double)_lambda.Invoke(_parameterValues); + } + + return sum; + } +} diff --git a/benchmark/DynamicExpresso.Benchmarks/Program.cs b/benchmark/DynamicExpresso.Benchmarks/Program.cs new file mode 100644 index 0000000..c9a0467 --- /dev/null +++ b/benchmark/DynamicExpresso.Benchmarks/Program.cs @@ -0,0 +1,3 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); From 3f9f4d179d0fbb900ad139ebeb07a9d7eaccbf5c Mon Sep 17 00:00:00 2001 From: David Zhang Date: Sat, 22 Nov 2025 02:51:17 -0800 Subject: [PATCH 3/5] Update Lambda class: optimize parameter handling and improve invocation logic --- src/DynamicExpresso.Core/Lambda.cs | 107 +++++++++++++++++------------ 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/src/DynamicExpresso.Core/Lambda.cs b/src/DynamicExpresso.Core/Lambda.cs index 27108ac..93fba00 100644 --- a/src/DynamicExpresso.Core/Lambda.cs +++ b/src/DynamicExpresso.Core/Lambda.cs @@ -22,11 +22,15 @@ public class Lambda private readonly bool _preferInterpretation; // Snapshots taken at construction time so we don't re-enumerate and allocate on every call. + private readonly Parameter[] _declaredParameters; private readonly Parameter[] _usedParameters; private readonly ParameterExpression[] _declaredParameterExpressions; // For each used parameter index, which declared parameter index it corresponds to. private readonly int[] _usedToDeclaredIndex; + private readonly bool _allUsedAndInDeclaredOrder; + private readonly Type[] _effectiveUsedTypes; + private readonly bool[] _usedAllowsNull; private readonly int _declaredCount; private readonly int _usedCount; @@ -42,11 +46,12 @@ internal Lambda(Expression expression, ParserArguments parserArguments, bool pre // Snapshot parameters once: avoids repeated enumeration/allocation. var declaredParameters = _parserArguments.DeclaredParameters.ToArray(); + _declaredParameters = declaredParameters; _usedParameters = _parserArguments.UsedParameters.ToArray(); - _declaredParameterExpressions = declaredParameters.Select(p => p.Expression).ToArray(); + _declaredParameterExpressions = _declaredParameters.Select(p => p.Expression).ToArray(); - _declaredCount = declaredParameters.Length; + _declaredCount = _declaredParameters.Length; _usedCount = _usedParameters.Length; _delegate = new Lazy(() => @@ -82,6 +87,37 @@ internal Lambda(Expression expression, ParserArguments parserArguments, bool pre _usedToDeclaredIndex = Array.Empty(); } + _allUsedAndInDeclaredOrder = + _usedCount == _declaredCount && + Enumerable.Range(0, _usedCount).All(i => _usedToDeclaredIndex[i] == i); + + if (_usedCount == 0) + { + _effectiveUsedTypes = Array.Empty(); + _usedAllowsNull = Array.Empty(); + } + else + { + _effectiveUsedTypes = new Type[_usedCount]; + _usedAllowsNull = new bool[_usedCount]; + + for (var i = 0; i < _usedCount; i++) + { + var t = _usedParameters[i].Type; + if (t == typeof(object)) + { + _effectiveUsedTypes[i] = typeof(object); + _usedAllowsNull[i] = true; + } + else + { + var underlying = Nullable.GetUnderlyingType(t); + _effectiveUsedTypes[i] = underlying ?? t; + _usedAllowsNull[i] = underlying != null || !t.IsValueType; + } + } + } + _fastInvokerFromDeclared = new Lazy>(BuildFastInvokerFromDeclared); } @@ -95,19 +131,19 @@ internal Lambda(Expression expression, ParserArguments parserArguments, bool pre /// /// The used parameters. [Obsolete("Use UsedParameters or DeclaredParameters")] - public IEnumerable Parameters { get { return _parserArguments.UsedParameters; } } + public IEnumerable Parameters { get { return _usedParameters; } } /// /// Gets the parameters actually used in the expression parsed. /// /// The used parameters. - public IEnumerable UsedParameters { get { return _parserArguments.UsedParameters; } } + public IEnumerable UsedParameters { get { return _usedParameters; } } /// /// Gets the parameters declared when parsing the expression. /// /// The declared parameters. - public IEnumerable DeclaredParameters { get { return _parserArguments.DeclaredParameters; } } + public IEnumerable DeclaredParameters { get { return _declaredParameters; } } public IEnumerable Types { get { return _parserArguments.UsedTypes; } } public IEnumerable Identifiers { get { return _parserArguments.UsedIdentifiers; } } @@ -240,6 +276,9 @@ private object[] BuildUsedArgsFromDeclared(object[] declaredArgs) if (_usedCount == 0) return Array.Empty(); + if (_allUsedAndInDeclaredOrder) + return declaredArgs; + var used = new object[_usedCount]; for (var i = 0; i < _usedCount; i++) { @@ -262,31 +301,26 @@ private bool CanUseFastInvoker(object[] declaredArgs) { var declaredIndex = _usedToDeclaredIndex[i]; var value = declaredArgs[declaredIndex]; - var targetType = _usedParameters[i].Type; - if (!IsDirectlyAssignable(value, targetType)) + if (!IsDirectlyAssignable(i, value)) return false; } return true; } - private static bool IsDirectlyAssignable(object value, Type targetType) + private bool IsDirectlyAssignable(int usedIndex, object value) { - if (targetType == typeof(object)) + if (_effectiveUsedTypes[usedIndex] == typeof(object)) return true; - var underlying = Nullable.GetUnderlyingType(targetType); - if (value == null) { // null is allowed for reference types and Nullable - return underlying != null || !targetType.IsValueType; + return _usedAllowsNull[usedIndex]; } - // If it's a Nullable, we allow values of T directly. - var effectiveType = underlying ?? targetType; - return effectiveType.IsInstanceOfType(value); + return _effectiveUsedTypes[usedIndex].IsInstanceOfType(value); } private Func BuildFastInvokerFromDeclared() @@ -294,26 +328,20 @@ private Func BuildFastInvokerFromDeclared() // Ensure the underlying delegate is compiled once. var del = _delegate.Value; var delType = del.GetType(); + var invokeMethod = delType.GetMethod("Invoke"); + if (invokeMethod == null) + throw new InvalidOperationException("Delegate Invoke method not found."); var argsParam = Expression.Parameter(typeof(object[]), "args"); + var target = Expression.Constant(del, delType); Expression body; - if (_usedCount == 0) { - var invokeExpr = Expression.Invoke(Expression.Constant(del, delType)); - if (invokeExpr.Type == typeof(void)) - { - body = Expression.Block(invokeExpr, Expression.Constant(null, typeof(object))); - } - else if (invokeExpr.Type.IsValueType) - { - body = Expression.Convert(invokeExpr, typeof(object)); - } - else - { - body = invokeExpr; - } + var call = Expression.Call(target, invokeMethod); + body = call.Type == typeof(void) + ? Expression.Block(call, Expression.Constant(null, typeof(object))) + : (Expression)Expression.Convert(call, typeof(object)); } else { @@ -330,24 +358,13 @@ private Func BuildFastInvokerFromDeclared() // We only use this fast path when IsDirectlyAssignable has already confirmed // that the runtime value is compatible with the target type, so this Convert // can't introduce new InvalidCastExceptions compared to DynamicInvoke. - var converted = Expression.Convert(accessExpr, _usedParameters[i].Type); - callArgs[i] = converted; + callArgs[i] = Expression.Convert(accessExpr, _usedParameters[i].Type); } - var invokeExpr = Expression.Invoke(Expression.Constant(del, delType), callArgs); - - if (invokeExpr.Type == typeof(void)) - { - body = Expression.Block(invokeExpr, Expression.Constant(null, typeof(object))); - } - else if (invokeExpr.Type.IsValueType) - { - body = Expression.Convert(invokeExpr, typeof(object)); - } - else - { - body = invokeExpr; - } + var call = Expression.Call(target, invokeMethod, callArgs); + body = call.Type == typeof(void) + ? Expression.Block(call, Expression.Constant(null, typeof(object))) + : (Expression)Expression.Convert(call, typeof(object)); } var lambda = Expression.Lambda>(body, argsParam); From 87b06f4bdab6b28607a7f7a64df6403b3449a113 Mon Sep 17 00:00:00 2001 From: David Zhang Date: Mon, 24 Nov 2025 22:00:03 -0800 Subject: [PATCH 4/5] Add benchmark for evaluating parameters using expression text --- .../LambdaBenchmarks.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs b/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs index 2212f18..ab4e079 100644 --- a/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs +++ b/benchmark/DynamicExpresso.Benchmarks/LambdaBenchmarks.cs @@ -5,6 +5,9 @@ namespace DynamicExpresso.Benchmarks; [MemoryDiagnoser] public class LambdaBenchmarks { + private const string ExpressionText = + "((a + b) * c - (double)d / (e + f + 1)) + Math.Max(a, b)"; + private Interpreter _interpreter = null!; private Lambda _lambda = null!; @@ -36,7 +39,7 @@ public void Setup() }; _lambda = _interpreter.Parse( - "((a + b) * c - (double)d / (e + f + 1)) + Math.Max(a, b)", + ExpressionText, typeof(double), _declared); @@ -76,4 +79,16 @@ public double Invoke_ParametersEnumerable() return sum; } + + [Benchmark(Description = "Eval (IEnumerable)")] + public double Eval_ParametersEnumerable() + { + double sum = 0; + for (var i = 0; i < 100_000; i++) + { + sum += _interpreter.Eval(ExpressionText, _parameterValues); + } + + return sum; + } } From 0dac8122c38530d58c1a924b73608ecb07f3733a Mon Sep 17 00:00:00 2001 From: David Zhang Date: Mon, 24 Nov 2025 23:28:08 -0800 Subject: [PATCH 5/5] Refactor Lambda invocation via InvocationContext --- src/DynamicExpresso.Core/Lambda.cs | 573 ++++++++++++++--------------- 1 file changed, 280 insertions(+), 293 deletions(-) diff --git a/src/DynamicExpresso.Core/Lambda.cs b/src/DynamicExpresso.Core/Lambda.cs index 93fba00..e07d153 100644 --- a/src/DynamicExpresso.Core/Lambda.cs +++ b/src/DynamicExpresso.Core/Lambda.cs @@ -16,109 +16,21 @@ public class Lambda { private readonly Expression _expression; private readonly ParserArguments _parserArguments; - - // Delegate whose parameters are in UsedParameters order. - private readonly Lazy _delegate; - private readonly bool _preferInterpretation; - - // Snapshots taken at construction time so we don't re-enumerate and allocate on every call. - private readonly Parameter[] _declaredParameters; - private readonly Parameter[] _usedParameters; - private readonly ParameterExpression[] _declaredParameterExpressions; - - // For each used parameter index, which declared parameter index it corresponds to. - private readonly int[] _usedToDeclaredIndex; - private readonly bool _allUsedAndInDeclaredOrder; - private readonly Type[] _effectiveUsedTypes; - private readonly bool[] _usedAllowsNull; - private readonly int _declaredCount; - private readonly int _usedCount; - - // Fast path: declared-order object[] -> result. - private readonly Lazy> _fastInvokerFromDeclared; + private readonly InvocationContext _invocation; internal Lambda(Expression expression, ParserArguments parserArguments, bool preferInterpretation = false) { _expression = expression ?? throw new ArgumentNullException(nameof(expression)); _parserArguments = parserArguments ?? throw new ArgumentNullException(nameof(parserArguments)); - _preferInterpretation = preferInterpretation; - - // Snapshot parameters once: avoids repeated enumeration/allocation. - var declaredParameters = _parserArguments.DeclaredParameters.ToArray(); - _declaredParameters = declaredParameters; - _usedParameters = _parserArguments.UsedParameters.ToArray(); - - _declaredParameterExpressions = _declaredParameters.Select(p => p.Expression).ToArray(); - - _declaredCount = _declaredParameters.Length; - _usedCount = _usedParameters.Length; - - _delegate = new Lazy(() => - Expression.Lambda(_expression, _usedParameters.Select(p => p.Expression).ToArray()) - .Compile(_preferInterpretation)); - - // Precompute used-index -> declared-index mapping for the fast path. - if (_usedCount > 0) - { - if (_declaredCount == 0) - throw new InvalidOperationException("Used parameters exist but there are no declared parameters."); - - var nameToDeclaredIndex = - new Dictionary(_declaredCount, _parserArguments.Settings.KeyComparer); - for (var i = 0; i < declaredParameters.Length; i++) - { - nameToDeclaredIndex[declaredParameters[i].Name] = i; - } - - _usedToDeclaredIndex = new int[_usedCount]; - for (var i = 0; i < _usedCount; i++) - { - var usedName = _usedParameters[i].Name; - if (!nameToDeclaredIndex.TryGetValue(usedName, out var declaredIndex)) - throw new InvalidOperationException( - $"Used parameter '{usedName}' was not found in declared parameters."); - - _usedToDeclaredIndex[i] = declaredIndex; - } - } - else - { - _usedToDeclaredIndex = Array.Empty(); - } - - _allUsedAndInDeclaredOrder = - _usedCount == _declaredCount && - Enumerable.Range(0, _usedCount).All(i => _usedToDeclaredIndex[i] == i); - - if (_usedCount == 0) - { - _effectiveUsedTypes = Array.Empty(); - _usedAllowsNull = Array.Empty(); - } - else - { - _effectiveUsedTypes = new Type[_usedCount]; - _usedAllowsNull = new bool[_usedCount]; - - for (var i = 0; i < _usedCount; i++) - { - var t = _usedParameters[i].Type; - if (t == typeof(object)) - { - _effectiveUsedTypes[i] = typeof(object); - _usedAllowsNull[i] = true; - } - else - { - var underlying = Nullable.GetUnderlyingType(t); - _effectiveUsedTypes[i] = underlying ?? t; - _usedAllowsNull[i] = underlying != null || !t.IsValueType; - } - } - } - - _fastInvokerFromDeclared = new Lazy>(BuildFastInvokerFromDeclared); + var settings = _parserArguments.Settings; + _invocation = new InvocationContext( + expression, + _parserArguments.DeclaredParameters.ToArray(), + _parserArguments.UsedParameters.ToArray(), + settings.KeyComparison, + settings.KeyComparer, + preferInterpretation); } public Expression Expression { get { return _expression; } } @@ -131,33 +43,26 @@ internal Lambda(Expression expression, ParserArguments parserArguments, bool pre /// /// The used parameters. [Obsolete("Use UsedParameters or DeclaredParameters")] - public IEnumerable Parameters { get { return _usedParameters; } } + public IEnumerable Parameters { get { return _invocation.UsedParameters; } } /// /// Gets the parameters actually used in the expression parsed. /// /// The used parameters. - public IEnumerable UsedParameters { get { return _usedParameters; } } + public IEnumerable UsedParameters { get { return _invocation.UsedParameters; } } /// /// Gets the parameters declared when parsing the expression. /// /// The declared parameters. - public IEnumerable DeclaredParameters { get { return _declaredParameters; } } + public IEnumerable DeclaredParameters { get { return _invocation.DeclaredParameters; } } public IEnumerable Types { get { return _parserArguments.UsedTypes; } } public IEnumerable Identifiers { get { return _parserArguments.UsedIdentifiers; } } public object Invoke() { - if (_usedCount == 0) - { - return _fastInvokerFromDeclared.Value(Array.Empty()); - } - - // Fallback: preserve the original behavior where missing parameters - // TargetParameterCountException is likely to be thrown. - return InvokeWithUsedParameters(Array.Empty()); + return _invocation.InvokeNoArgs(); } public object Invoke(params Parameter[] parameters) @@ -171,72 +76,201 @@ public object Invoke(params Parameter[] parameters) /// public object Invoke(IEnumerable parameters) { - if (parameters == null) - throw new ArgumentNullException(nameof(parameters)); + return _invocation.InvokeFromNamed(parameters); + } + + /// + /// Invoke the expression with the given parameter values. + /// The values are in the same order as the parameters declared when parsing (DeclaredParameters). + /// Only the parameters actually used in the expression are passed to the underlying delegate. + /// + /// Values for declared parameters, in declared order. + public object Invoke(params object[] args) + { + return _invocation.InvokeFromDeclared(args); + } - var paramList = parameters as IList ?? parameters.ToArray(); - var matchedValues = new List(_usedCount); + public override string ToString() + { + return ExpressionText; + } - foreach (var used in _usedParameters) + /// + /// Generate the given delegate by compiling the lambda expression. + /// + /// + /// The delegate to generate. Delegate parameters must match the ones defined + /// when creating the expression, see DeclaredParameters. + /// + public TDelegate Compile() + { + var lambdaExpression = LambdaExpression(); + return lambdaExpression.Compile(); + } + + [Obsolete("Use Compile()")] + public TDelegate Compile(IEnumerable parameters) + { + var lambdaExpression = Expression.Lambda(_expression, parameters.Select(p => p.Expression).ToArray()); + return lambdaExpression.Compile(); + } + + /// + /// Generate a lambda expression. + /// + /// The lambda expression. + /// + /// The delegate to generate. Delegate parameters must match the ones defined + /// when creating the expression, see DeclaredParameters. + /// + public Expression LambdaExpression() + { + return Expression.Lambda(_expression, _invocation.DeclaredParameterExpressions); + } + + internal LambdaExpression LambdaExpression(Type delegateType) + { + var parameterExpressions = _invocation.DeclaredParameterExpressions; + var types = delegateType.GetGenericArguments(); + + // return type + var genericType = delegateType.GetGenericTypeDefinition(); + if (genericType == ReflectionExtensions.GetFuncType(parameterExpressions.Length)) + types[types.Length - 1] = _expression.Type; + + var inferredDelegateType = genericType.MakeGenericType(types); + return Expression.Lambda(inferredDelegateType, _expression, parameterExpressions); + } + + private sealed class InvocationContext + { + private readonly Expression _expression; + private readonly Parameter[] _declaredParameters; + private readonly Parameter[] _usedParameters; + private readonly StringComparison _keyComparison; + private readonly IEqualityComparer _keyComparer; + private readonly bool _preferInterpretation; + + // used index -> declared index + private readonly int[] _usedToDeclaredIndex; + + // Delegate with parameters in UsedParameters order. + private readonly Lazy _delegate; + + // Fast path: declared-order object[] -> result. + private readonly Lazy> _fastInvokerFromDeclared; + + public InvocationContext( + Expression expression, + Parameter[] declaredParameters, + Parameter[] usedParameters, + StringComparison keyComparison, + IEqualityComparer keyComparer, + bool preferInterpretation) { - foreach (var actual in paramList) - { - if (actual != null && - used.Name.Equals(actual.Name, _parserArguments.Settings.KeyComparison)) - { - matchedValues.Add(actual.Value); - } - } + _expression = expression ?? throw new ArgumentNullException(nameof(expression)); + _declaredParameters = declaredParameters ?? Array.Empty(); + _usedParameters = usedParameters ?? Array.Empty(); + _keyComparison = keyComparison; + _keyComparer = keyComparer ?? StringComparer.InvariantCulture; + _preferInterpretation = preferInterpretation; + + DeclaredParameterExpressions = _declaredParameters.Select(p => p.Expression).ToArray(); + + _delegate = new Lazy(() => + Expression.Lambda(_expression, _usedParameters.Select(p => p.Expression).ToArray()) + .Compile(_preferInterpretation)); + + _usedToDeclaredIndex = BuildUsedToDeclaredIndex(_declaredParameters, _usedParameters, _keyComparer); + + _fastInvokerFromDeclared = new Lazy>(BuildFastInvokerFromDeclared); } - if (_usedCount == 0) + public Parameter[] DeclaredParameters => _declaredParameters; + public Parameter[] UsedParameters => _usedParameters; + public ParameterExpression[] DeclaredParameterExpressions { get; } + + public object InvokeNoArgs() { - return _fastInvokerFromDeclared.Value(Array.Empty()); + if (_usedParameters.Length == 0) + return _fastInvokerFromDeclared.Value(Array.Empty()); + + return InvokeWithUsedParameters(Array.Empty()); } - if (matchedValues.Count == _usedCount) + public object InvokeFromDeclared(object[] args) { - var declaredArgs = new object[_declaredCount]; - for (var i = 0; i < _usedCount; i++) + if (args == null) + return InvokeNoArgs(); + + if (_declaredParameters.Length != args.Length) + throw new InvalidOperationException(ErrorMessages.ArgumentCountMismatch); + + if (_usedParameters.Length == 0) + return _fastInvokerFromDeclared.Value(Array.Empty()); + + if (CanUseFastInvoker(args)) { - var declaredIndex = _usedToDeclaredIndex[i]; - declaredArgs[declaredIndex] = matchedValues[i]; + try + { + return _fastInvokerFromDeclared.Value(args); + } + catch (TargetInvocationException exc) + { + if (exc.InnerException != null) + ExceptionDispatchInfo.Capture(exc.InnerException).Throw(); + + throw; + } } - return Invoke(declaredArgs); + var usedArgs = BuildUsedArgsFromDeclared(args); + return InvokeWithUsedParameters(usedArgs); } - return InvokeWithUsedParameters(matchedValues.ToArray()); - } - - /// - /// Invoke the expression with the given parameter values. - /// The values are in the same order as the parameters declared when parsing (DeclaredParameters). - /// Only the parameters actually used in the expression are passed to the underlying delegate. - /// - /// Values for declared parameters, in declared order. - public object Invoke(params object[] args) - { - if (args == null) + public object InvokeFromNamed(IEnumerable parameters) { - return Invoke(); - } + if (parameters == null) + throw new ArgumentNullException(nameof(parameters)); - if (_declaredCount != args.Length) - throw new InvalidOperationException(ErrorMessages.ArgumentCountMismatch); + if (_usedParameters.Length == 0) + return _fastInvokerFromDeclared.Value(Array.Empty()); - // No parameters are actually used: ignore any supplied values. - if (_usedCount == 0) - { - return _fastInvokerFromDeclared.Value(Array.Empty()); + var paramList = parameters as IList ?? parameters.ToArray(); + var matchedValues = new List(_usedParameters.Length); + + foreach (var used in _usedParameters) + { + foreach (var actual in paramList) + { + if (actual != null && + string.Equals(used.Name, actual.Name, _keyComparison)) + { + matchedValues.Add(actual.Value); + } + } + } + + if (matchedValues.Count == _usedParameters.Length) + { + var declaredArgs = new object[_declaredParameters.Length]; + for (var i = 0; i < _usedParameters.Length; i++) + { + var declaredIndex = _usedToDeclaredIndex[i]; + declaredArgs[declaredIndex] = matchedValues[i]; + } + + return InvokeFromDeclared(declaredArgs); + } + + return InvokeWithUsedParameters(matchedValues.ToArray()); } - // Fast path: all values already directly assignable to the expected parameter types. - if (CanUseFastInvoker(args)) + private object InvokeWithUsedParameters(object[] orderedArgs) { try { - return _fastInvokerFromDeclared.Value(args); + return _delegate.Value.DynamicInvoke(orderedArgs); } catch (TargetInvocationException exc) { @@ -247,180 +281,133 @@ public object Invoke(params object[] args) } } - var usedArgs = BuildUsedArgsFromDeclared(args); - return InvokeWithUsedParameters(usedArgs); - } - - /// - /// orderedUsedArgs must be in UsedParameters order (the same order used to compile _delegate). - /// This method preserves the original DynamicInvoke-based behavior, including exception types - /// for mismatched argument counts and conversion failures. - /// - private object InvokeWithUsedParameters(object[] orderedArgs) - { - try + private static int[] BuildUsedToDeclaredIndex( + IReadOnlyList declaredParameters, + IReadOnlyList usedParameters, + IEqualityComparer keyComparer) { - return _delegate.Value.DynamicInvoke(orderedArgs); - } - catch (TargetInvocationException exc) - { - if (exc.InnerException != null) - ExceptionDispatchInfo.Capture(exc.InnerException).Throw(); + if (usedParameters.Count == 0) + return Array.Empty(); - throw; - } - } + if (declaredParameters.Count == 0) + throw new InvalidOperationException("Used parameters exist but there are no declared parameters."); - private object[] BuildUsedArgsFromDeclared(object[] declaredArgs) - { - if (_usedCount == 0) - return Array.Empty(); + var nameToDeclaredIndex = new Dictionary(declaredParameters.Count, keyComparer); + for (var i = 0; i < declaredParameters.Count; i++) + { + nameToDeclaredIndex[declaredParameters[i].Name] = i; + } - if (_allUsedAndInDeclaredOrder) - return declaredArgs; + var map = new int[usedParameters.Count]; + for (var i = 0; i < usedParameters.Count; i++) + { + var usedName = usedParameters[i].Name; + if (!nameToDeclaredIndex.TryGetValue(usedName, out var declaredIndex)) + throw new InvalidOperationException( + $"Used parameter '{usedName}' was not found in declared parameters."); - var used = new object[_usedCount]; - for (var i = 0; i < _usedCount; i++) - { - var declaredIndex = _usedToDeclaredIndex[i]; - used[i] = declaredArgs[declaredIndex]; + map[i] = declaredIndex; + } + + return map; } - return used; - } + private object[] BuildUsedArgsFromDeclared(object[] declaredArgs) + { + if (_usedParameters.Length == 0) + return Array.Empty(); - private bool CanUseFastInvoker(object[] declaredArgs) - { - if (_usedCount == 0) - return true; + var used = new object[_usedParameters.Length]; + for (var i = 0; i < _usedParameters.Length; i++) + { + var declaredIndex = _usedToDeclaredIndex[i]; + used[i] = declaredArgs[declaredIndex]; + } - if (declaredArgs == null || declaredArgs.Length != _declaredCount) - return false; + return used; + } - for (var i = 0; i < _usedCount; i++) + private bool CanUseFastInvoker(object[] declaredArgs) { - var declaredIndex = _usedToDeclaredIndex[i]; - var value = declaredArgs[declaredIndex]; + if (_usedParameters.Length == 0) + return true; - if (!IsDirectlyAssignable(i, value)) + if (declaredArgs == null || declaredArgs.Length != _declaredParameters.Length) return false; - } - return true; - } + for (var i = 0; i < _usedParameters.Length; i++) + { + var declaredIndex = _usedToDeclaredIndex[i]; + var value = declaredArgs[declaredIndex]; + var targetType = _usedParameters[i].Type; - private bool IsDirectlyAssignable(int usedIndex, object value) - { - if (_effectiveUsedTypes[usedIndex] == typeof(object)) - return true; + if (!IsDirectlyAssignable(targetType, value)) + return false; + } - if (value == null) - { - // null is allowed for reference types and Nullable - return _usedAllowsNull[usedIndex]; + return true; } - return _effectiveUsedTypes[usedIndex].IsInstanceOfType(value); - } - - private Func BuildFastInvokerFromDeclared() - { - // Ensure the underlying delegate is compiled once. - var del = _delegate.Value; - var delType = del.GetType(); - var invokeMethod = delType.GetMethod("Invoke"); - if (invokeMethod == null) - throw new InvalidOperationException("Delegate Invoke method not found."); - - var argsParam = Expression.Parameter(typeof(object[]), "args"); - var target = Expression.Constant(del, delType); - - Expression body; - if (_usedCount == 0) - { - var call = Expression.Call(target, invokeMethod); - body = call.Type == typeof(void) - ? Expression.Block(call, Expression.Constant(null, typeof(object))) - : (Expression)Expression.Convert(call, typeof(object)); - } - else + private static bool IsDirectlyAssignable(Type targetType, object value) { - var callArgs = new Expression[_usedCount]; + if (targetType == typeof(object)) + return true; - for (var i = 0; i < _usedCount; i++) + if (value == null) { - var declaredIndex = _usedToDeclaredIndex[i]; + if (!targetType.IsValueType) + return true; - // args[declaredIndex] - var indexExpr = Expression.Constant(declaredIndex); - var accessExpr = Expression.ArrayIndex(argsParam, indexExpr); - - // We only use this fast path when IsDirectlyAssignable has already confirmed - // that the runtime value is compatible with the target type, so this Convert - // can't introduce new InvalidCastExceptions compared to DynamicInvoke. - callArgs[i] = Expression.Convert(accessExpr, _usedParameters[i].Type); + return Nullable.GetUnderlyingType(targetType) != null; } - var call = Expression.Call(target, invokeMethod, callArgs); - body = call.Type == typeof(void) - ? Expression.Block(call, Expression.Constant(null, typeof(object))) - : (Expression)Expression.Convert(call, typeof(object)); + var underlying = Nullable.GetUnderlyingType(targetType); + var effectiveType = underlying ?? targetType; + return effectiveType.IsInstanceOfType(value); } - var lambda = Expression.Lambda>(body, argsParam); - return lambda.Compile(_preferInterpretation); - } + private Func BuildFastInvokerFromDeclared() + { + var del = _delegate.Value; + var delType = del.GetType(); + var invokeMethod = delType.GetMethod("Invoke"); + if (invokeMethod == null) + throw new InvalidOperationException("Delegate Invoke method not found."); - public override string ToString() - { - return ExpressionText; - } + var argsParam = Expression.Parameter(typeof(object[]), "args"); + var target = Expression.Constant(del, delType); - /// - /// Generate the given delegate by compiling the lambda expression. - /// - /// - /// The delegate to generate. Delegate parameters must match the ones defined - /// when creating the expression, see DeclaredParameters. - /// - public TDelegate Compile() - { - var lambdaExpression = LambdaExpression(); - return lambdaExpression.Compile(); - } + Expression body; + if (_usedParameters.Length == 0) + { + var call = Expression.Call(target, invokeMethod); + body = call.Type == typeof(void) + ? Expression.Block(call, Expression.Constant(null, typeof(object))) + : (Expression)Expression.Convert(call, typeof(object)); + } + else + { + var callArgs = new Expression[_usedParameters.Length]; - [Obsolete("Use Compile()")] - public TDelegate Compile(IEnumerable parameters) - { - var lambdaExpression = Expression.Lambda(_expression, parameters.Select(p => p.Expression).ToArray()); - return lambdaExpression.Compile(); - } + for (var i = 0; i < _usedParameters.Length; i++) + { + var declaredIndex = _usedToDeclaredIndex[i]; - /// - /// Generate a lambda expression. - /// - /// The lambda expression. - /// - /// The delegate to generate. Delegate parameters must match the ones defined - /// when creating the expression, see DeclaredParameters. - /// - public Expression LambdaExpression() - { - return Expression.Lambda(_expression, _declaredParameterExpressions); - } + var indexExpr = Expression.Constant(declaredIndex); + var accessExpr = Expression.ArrayIndex(argsParam, indexExpr); - internal LambdaExpression LambdaExpression(Type delegateType) - { - var parameterExpressions = _declaredParameterExpressions; - var types = delegateType.GetGenericArguments(); + callArgs[i] = Expression.Convert(accessExpr, _usedParameters[i].Type); + } - // return type - var genericType = delegateType.GetGenericTypeDefinition(); - if (genericType == ReflectionExtensions.GetFuncType(parameterExpressions.Length)) - types[types.Length - 1] = _expression.Type; + var call = Expression.Call(target, invokeMethod, callArgs); + body = call.Type == typeof(void) + ? Expression.Block(call, Expression.Constant(null, typeof(object))) + : (Expression)Expression.Convert(call, typeof(object)); + } - var inferredDelegateType = genericType.MakeGenericType(types); - return Expression.Lambda(inferredDelegateType, _expression, parameterExpressions); + var lambda = Expression.Lambda>(body, argsParam); + return lambda.Compile(_preferInterpretation); + } } } }