From 472e5be31b00e3c21e1d47ee5f4b5197cbb61c74 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 3 Nov 2026 13:56:47 +0100 Subject: [PATCH 1/3] fix: compile reducible custom expression node kinds Use CanReduce and ReduceAndCheck for unknown System expression node kinds in the existing collector and emitter. Keep validation in a private cold helper; preserve label operand scope. Test reduction chains, captured constants and invalid result types. --- .../FastExpressionCompiler.cs | 24 +++++++++ .../Program.cs | 1 + .../Program.cs | 1 + .../CustomReducibleNodeRegressionTests.cs | 50 +++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 test/FastExpressionCompiler.UnitTests/CustomReducibleNodeRegressionTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 8ee51c68..59525547 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -1752,11 +1752,27 @@ public static Result TryCollectInfo(ref CompilerContext context, Expression expr continue; } +#if !LIGHT_EXPRESSION + var customReduced = TryReduceCustomNode(expr); + if (customReduced != null) + { + expr = customReduced; + enterable = false; + continue; + } +#endif return Result.NotSupported_UnknownExpression; } } } +#if !LIGHT_EXPRESSION + // Unknown node kinds are a cold path; leave reduction validation outside the common visitors. + [MethodImpl(MethodImplOptions.NoInlining)] + private static Expression TryReduceCustomNode(Expression expression) => + expression.CanReduce ? expression.ReduceAndCheck() : null; +#endif + private static Expression CreateInlinedLambdaInvocationExpression(ArgExprs invokeArgs, LambdaExpression lambdaExpr) { var invokeArgCount = invokeArgs.GetArgCount(); @@ -2692,6 +2708,14 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext case ExpressionType.Quote: // todo: @feature - is not supported yet default: +#if !LIGHT_EXPRESSION + var customReduced = TryReduceCustomNode(expr); + if (customReduced != null) + { + expr = customReduced; + continue; + } +#endif return false; } diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index eaeefdf4..0ab7d52f 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -66,6 +66,7 @@ static void RunLightExpressionTests(object state) fecTests.Run(new Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); fecTests.Run(new ReductionProgressRegressionTests()); fecTests.Run(new RuntimeTypeConstantRegressionTests()); + fecTests.Run(new CustomReducibleNodeRegressionTests()); fecTests.Run(new ParityTests()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 7a867a16..02ec4ed1 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -85,6 +85,7 @@ public static void Main(string[] args) st.Run(new Issue561_NonBooleanEqualityOperator()); st.Run(new ReductionProgressRegressionTests()); st.Run(new RuntimeTypeConstantRegressionTests()); + st.Run(new CustomReducibleNodeRegressionTests()); st.Run(new ParityTests()); st.Run(new Issue498_InvalidProgramException_when_using_loop()); st.Run(new Issue495_Incomplete_pattern_detection_for_NotSupported_1007_Return_goto_from_TryCatch_with_Assign_generates_invalid_IL()); diff --git a/test/FastExpressionCompiler.UnitTests/CustomReducibleNodeRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/CustomReducibleNodeRegressionTests.cs new file mode 100644 index 00000000..b1e030b2 --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/CustomReducibleNodeRegressionTests.cs @@ -0,0 +1,50 @@ +using System; +#if !LIGHT_EXPRESSION +using System.Linq.Expressions; +namespace FastExpressionCompiler.UnitTests; +public struct CustomReducibleNodeRegressionTests : ITestX +{ + public void Run(TestRun t) { Custom_node_type_reduces_before_emission(t); Reduction_collects_captured_constants_and_chains(t); Invalid_reductions_use_system_validation(t); } + public void Custom_node_type_reduces_before_emission(TestContext t) + { + var expression = Expression.Lambda>(new Reducible(Expression.Constant(3))); + var fast = expression.CompileFast(true); + if (t.IsNotNull(fast)) + { +#if NETFRAMEWORK + t.AreEqual(3, fast()); // Framework System does not reduce unknown node kinds. +#else + t.AreEqual(expression.Compile()(), fast()); +#endif + } + } + public void Reduction_collects_captured_constants_and_chains(TestContext t) + { + var marker = new object(); + var expression = Expression.Lambda>(new Reducible(new Reducible(Expression.Constant(marker)))); + var fast = expression.CompileFast(true); + if (t.IsNotNull(fast)) t.AreEqual(true, ReferenceEquals(marker, fast())); + } + public void Invalid_reductions_use_system_validation(TestContext t) + { + var source = Expression.Lambda>(new InvalidReduction()); + t.Throws(() => source.CompileFast(true)); + } + private sealed class InvalidReduction : Expression + { + public override Type Type => typeof(int); + public override ExpressionType NodeType => (ExpressionType)(-1); + public override bool CanReduce => true; + public override Expression Reduce() => Expression.Constant("wrong type"); + } + public class Reducible : Expression + { + private readonly Expression _reduced; + public Reducible(Expression reduced) => _reduced = reduced; + public override Type Type => _reduced.Type; + public override ExpressionType NodeType => (ExpressionType)(-1); + public override bool CanReduce => true; + public override Expression Reduce() => _reduced; + } +} +#endif From 0559f5478164589c5f14aeb45e2a091fe7c99750 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Tue, 3 Nov 2026 22:25:02 +0100 Subject: [PATCH 2/3] fix: reuse custom node reductions during emission Keep reduced trees in a lazy per-compilation reference-identity cache so collection and emission observe the same locals, constants, and nested lambdas. Preserve ReduceAndCheck validation and leave LightExpression unchanged. Add regressions for fresh reductions, shared and distinct node identities, repeated compilation, and emission without collection. Verified all regression cases on net8.0, net10.0 and net472; full Release build passes. Existing master nullable MultiplyChecked parity failures remain unchanged. --- .../FastExpressionCompiler.cs | 37 ++++++- .../CustomReducibleNodeRegressionTests.cs | 103 +++++++++++++++++- 2 files changed, 134 insertions(+), 6 deletions(-) diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 59525547..61fa60a4 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -649,6 +649,10 @@ public struct CompilerContext /// The map of inlined invocations collected in TryCollect and then used in TryEmit internal SmallMap4> InlinedLambdaInvocation; +#if !LIGHT_EXPRESSION + internal CustomNodeReductionCache CustomNodeReductions; +#endif + /// New or Call expressions containing the complex expression, e.g. inlined Lambda Invoke or Try with Finally internal SmallSet4> ArgsContainingComplexExpression; @@ -1753,7 +1757,7 @@ public static Result TryCollectInfo(ref CompilerContext context, Expression expr } #if !LIGHT_EXPRESSION - var customReduced = TryReduceCustomNode(expr); + var customReduced = TryReduceCustomNode(expr, ref context); if (customReduced != null) { expr = customReduced; @@ -1767,10 +1771,33 @@ public static Result TryCollectInfo(ref CompilerContext context, Expression expr } #if !LIGHT_EXPRESSION - // Unknown node kinds are a cold path; leave reduction validation outside the common visitors. + internal sealed class CustomNodeReductionCache + { + internal SmallMap4> Reductions; + } + + // Collection and emission must use the same reduced tree: Reduce may create fresh locals, + // constants, or nested lambdas. Allocate the cache only for reducible custom node kinds. [MethodImpl(MethodImplOptions.NoInlining)] - private static Expression TryReduceCustomNode(Expression expression) => - expression.CanReduce ? expression.ReduceAndCheck() : null; + private static Expression TryReduceCustomNode(Expression expression, ref CompilerContext context) + { + var cache = context.CustomNodeReductions; + if (cache != null) + { + ref var cached = ref cache.Reductions.Map.TryGetValueRef(expression, out var found); + if (found) + return cached; + } + + if (!expression.CanReduce) + return null; + + var reduced = expression.ReduceAndCheck(); + if (cache == null) + context.CustomNodeReductions = cache = new CustomNodeReductionCache(); + cache.Reductions.Map.AddOrGetValueRef(expression, out _) = reduced; + return reduced; + } #endif private static Expression CreateInlinedLambdaInvocationExpression(ArgExprs invokeArgs, LambdaExpression lambdaExpr) @@ -2709,7 +2736,7 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext case ExpressionType.Quote: // todo: @feature - is not supported yet default: #if !LIGHT_EXPRESSION - var customReduced = TryReduceCustomNode(expr); + var customReduced = TryReduceCustomNode(expr, ref context); if (customReduced != null) { expr = customReduced; diff --git a/test/FastExpressionCompiler.UnitTests/CustomReducibleNodeRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/CustomReducibleNodeRegressionTests.cs index b1e030b2..3f8e5c3b 100644 --- a/test/FastExpressionCompiler.UnitTests/CustomReducibleNodeRegressionTests.cs +++ b/test/FastExpressionCompiler.UnitTests/CustomReducibleNodeRegressionTests.cs @@ -4,7 +4,17 @@ namespace FastExpressionCompiler.UnitTests; public struct CustomReducibleNodeRegressionTests : ITestX { - public void Run(TestRun t) { Custom_node_type_reduces_before_emission(t); Reduction_collects_captured_constants_and_chains(t); Invalid_reductions_use_system_validation(t); } + public void Run(TestRun t) + { + Custom_node_type_reduces_before_emission(t); + Reduction_collects_captured_constants_and_chains(t); + Invalid_reductions_use_system_validation(t); + Fresh_block_locals_and_constants_are_shared_between_passes(t); + Fresh_nested_lambdas_are_shared_between_passes(t); + Shared_nodes_and_distinct_nodes_preserve_reference_identity(t); + Reduction_cache_is_local_to_each_compilation(t); + Emission_without_collection_can_reduce_custom_nodes(t); + } public void Custom_node_type_reduces_before_emission(TestContext t) { var expression = Expression.Lambda>(new Reducible(Expression.Constant(3))); @@ -30,6 +40,97 @@ public void Invalid_reductions_use_system_validation(TestContext t) var source = Expression.Lambda>(new InvalidReduction()); t.Throws(() => source.CompileFast(true)); } + private static readonly CompilerFlags[] Flags = + { + CompilerFlags.Default, CompilerFlags.NoInvocationLambdaInlining, + CompilerFlags.EnableDelegateDebugInfo | CompilerFlags.ThrowOnNotSupportedExpression + }; + public void Fresh_block_locals_and_constants_are_shared_between_passes(TestContext t) + { + foreach (var flags in Flags) + { + var node = new FreshBlock(41); + var fast = Expression.Lambda>(node).CompileFast(true, flags); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()); + t.AreEqual(1, node.Reductions); + } + } + public void Fresh_nested_lambdas_are_shared_between_passes(TestContext t) + { + foreach (var flags in Flags) + { + var node = new FreshNestedLambda(); + var fast = Expression.Lambda>>(node).CompileFast(true, flags); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()()); + t.AreEqual(1, node.Reductions); + } + } + public void Shared_nodes_and_distinct_nodes_preserve_reference_identity(TestContext t) + { + foreach (var flags in Flags) + { + var shared = new FreshBlock(20); + var same = Expression.Lambda>(Expression.Add(shared, shared)).CompileFast(true, flags); + if (t.IsNotNull(same)) t.AreEqual(42, same()); + t.AreEqual(1, shared.Reductions); + // User equality must not make two different custom nodes share a reduction. + var left = new FreshBlock(20); var right = new FreshBlock(21); + var distinct = Expression.Lambda>(Expression.Add(left, right)).CompileFast(true, flags); + if (t.IsNotNull(distinct)) t.AreEqual(43, distinct()); + t.AreEqual(1, left.Reductions); t.AreEqual(1, right.Reductions); + } + } + public void Reduction_cache_is_local_to_each_compilation(TestContext t) + { + var node = new FreshBlock(41); + var source = Expression.Lambda>(node); + for (var i = 1; i <= 2; ++i) + { + var fast = source.CompileFast(true); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()); + t.AreEqual(i, node.Reductions); + } + } + public void Emission_without_collection_can_reduce_custom_nodes(TestContext t) + { + var source = Expression.Lambda>(new Reducible(Expression.Constant(42))); + var fast = source.TryCompileWithoutClosure>(); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()); + } + public sealed class Marker { public int Value; } + private sealed class FreshBlock : Expression + { + private readonly int _value; + public int Reductions; + public FreshBlock(int value) => _value = value; + public override Type Type => typeof(int); + public override ExpressionType NodeType => (ExpressionType)(-1); + public override bool CanReduce => true; + public override Expression Reduce() + { + ++Reductions; + var local = Expression.Variable(typeof(Marker), "fresh"); + return Expression.Block(new[] { local }, + Expression.Assign(local, Expression.Constant(new Marker { Value = _value })), + Expression.Add(Expression.Field(local, nameof(Marker.Value)), Expression.Constant(1))); + } + public override bool Equals(object obj) => obj is FreshBlock; + public override int GetHashCode() => 0; + } + private sealed class FreshNestedLambda : Expression + { + public int Reductions; + public override Type Type => typeof(Func); + public override ExpressionType NodeType => (ExpressionType)(-1); + public override bool CanReduce => true; + public override Expression Reduce() + { + ++Reductions; + var local = Expression.Variable(typeof(int), "fresh"); + return Expression.Block(new[] { local }, Expression.Assign(local, Expression.Constant(41)), + Expression.Lambda>(Expression.Add(local, Expression.Constant(1)))); + } + } private sealed class InvalidReduction : Expression { public override Type Type => typeof(int); From 183ea00043b1cb491033fcae2897771b90405ac6 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Wed, 4 Nov 2026 06:41:05 +0100 Subject: [PATCH 3/3] fix: reuse extension reductions in both expression compilers Extend the lazy per-compilation reduction cache from #579 to ExpressionType.Extension for Standard and LightExpression. Collection and emission use the same reduced locals, constants, labels and nested lambdas. Preserve System validation for custom node kinds and existing null/self reduction rejection; intrinsic callbacks remain unchanged. Retain the parent per-node reference-identity policy: shared nodes reduce once per lambda compilation, distinct nodes stay separate, and later compilations get fresh reductions. Update the existing progress regression to assert reuse of a valid first reduction instead of requiring a second emission-time reduction. New regressions reproduce strict compilation failures on the parent and pass on net8.0, net10.0 and net472. Full solution, legacy suites and Framework runner pass; modern full runners retain only the existing nullable MultiplyChecked parity failures. Measured working controls emit identical IL and retain invocation allocations. Single-extension compilation adds a 120-byte lazy cache; ordinary controls retain compilation allocations. --- .../FastExpressionCompiler.cs | 41 ++-- .../Program.cs | 2 + .../Program.cs | 2 + ...tensionReductionIdentityRegressionTests.cs | 210 ++++++++++++++++++ .../ReductionProgressRegressionTests.cs | 10 +- 5 files changed, 242 insertions(+), 23 deletions(-) create mode 100644 test/FastExpressionCompiler.UnitTests/ExtensionReductionIdentityRegressionTests.cs diff --git a/src/FastExpressionCompiler/FastExpressionCompiler.cs b/src/FastExpressionCompiler/FastExpressionCompiler.cs index 61fa60a4..6a1c0b69 100644 --- a/src/FastExpressionCompiler/FastExpressionCompiler.cs +++ b/src/FastExpressionCompiler/FastExpressionCompiler.cs @@ -649,9 +649,7 @@ public struct CompilerContext /// The map of inlined invocations collected in TryCollect and then used in TryEmit internal SmallMap4> InlinedLambdaInvocation; -#if !LIGHT_EXPRESSION - internal CustomNodeReductionCache CustomNodeReductions; -#endif + internal ExpressionReductionCache ReducedExpressions; /// New or Call expressions containing the complex expression, e.g. inlined Lambda Invoke or Try with Finally internal SmallSet4> ArgsContainingComplexExpression; @@ -1703,8 +1701,8 @@ public static Result TryCollectInfo(ref CompilerContext context, Expression expr return r; case ExpressionType.Extension: - var reduced = expr.Reduce(); - if (reduced == null || ReferenceEquals(reduced, expr)) + var reduced = TryReduceNode(expr, ref context); + if (reduced == null) return Result.NotSupported_UnknownExpression; expr = reduced; enterable = false; @@ -1757,7 +1755,7 @@ public static Result TryCollectInfo(ref CompilerContext context, Expression expr } #if !LIGHT_EXPRESSION - var customReduced = TryReduceCustomNode(expr, ref context); + var customReduced = TryReduceNode(expr, ref context); if (customReduced != null) { expr = customReduced; @@ -1770,18 +1768,17 @@ public static Result TryCollectInfo(ref CompilerContext context, Expression expr } } -#if !LIGHT_EXPRESSION - internal sealed class CustomNodeReductionCache + internal sealed class ExpressionReductionCache { internal SmallMap4> Reductions; } // Collection and emission must use the same reduced tree: Reduce may create fresh locals, - // constants, or nested lambdas. Allocate the cache only for reducible custom node kinds. + // constants, labels, or nested lambdas. Allocate the cache only when reducing a node. [MethodImpl(MethodImplOptions.NoInlining)] - private static Expression TryReduceCustomNode(Expression expression, ref CompilerContext context) + private static Expression TryReduceNode(Expression expression, ref CompilerContext context) { - var cache = context.CustomNodeReductions; + var cache = context.ReducedExpressions; if (cache != null) { ref var cached = ref cache.Reductions.Map.TryGetValueRef(expression, out var found); @@ -1789,16 +1786,22 @@ private static Expression TryReduceCustomNode(Expression expression, ref Compile return cached; } - if (!expression.CanReduce) +#if !LIGHT_EXPRESSION + // System's unknown node kinds use its validation contract. Extension nodes keep + // the existing Reduce/null/self contract shared with LightExpression. + var reduced = expression.NodeType != ExpressionType.Extension + ? expression.CanReduce ? expression.ReduceAndCheck() : null + : expression.Reduce(); +#else + var reduced = expression.Reduce(); +#endif + if (reduced == null || ReferenceEquals(reduced, expression)) return null; - - var reduced = expression.ReduceAndCheck(); if (cache == null) - context.CustomNodeReductions = cache = new CustomNodeReductionCache(); + context.ReducedExpressions = cache = new ExpressionReductionCache(); cache.Reductions.Map.AddOrGetValueRef(expression, out _) = reduced; return reduced; } -#endif private static Expression CreateInlinedLambdaInvocationExpression(ArgExprs invokeArgs, LambdaExpression lambdaExpr) { @@ -2724,8 +2727,8 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext return TryEmitSwitch((SwitchExpression)expr, il, ref context, parent); case ExpressionType.Extension: - var reduced = expr.Reduce(); - if (reduced == null || ReferenceEquals(reduced, expr)) + var reduced = TryReduceNode(expr, ref context); + if (reduced == null) return false; expr = reduced; continue; @@ -2736,7 +2739,7 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext case ExpressionType.Quote: // todo: @feature - is not supported yet default: #if !LIGHT_EXPRESSION - var customReduced = TryReduceCustomNode(expr, ref context); + var customReduced = TryReduceNode(expr, ref context); if (customReduced != null) { expr = customReduced; diff --git a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs index 0ab7d52f..87cb23b2 100644 --- a/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner.Net472/Program.cs @@ -44,6 +44,7 @@ static void RunLightExpressionTests(object state) t.Run(new LightExpression.IssueTests.Issue498_InvalidProgramException_when_using_loop()); t.Run(new LightExpression.IssueTests.Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); t.Run(new LightExpression.UnitTests.RuntimeTypeConstantRegressionTests()); + t.Run(new LightExpression.UnitTests.ExtensionReductionIdentityRegressionTests()); t.Run(new LightExpression.UnitTests.ParityTests()); Console.WriteLine($"Just LightExpression tests are passing in {justLightTestsStopwatch.ElapsedMilliseconds} ms."); @@ -67,6 +68,7 @@ static void RunLightExpressionTests(object state) fecTests.Run(new ReductionProgressRegressionTests()); fecTests.Run(new RuntimeTypeConstantRegressionTests()); fecTests.Run(new CustomReducibleNodeRegressionTests()); + fecTests.Run(new ExtensionReductionIdentityRegressionTests()); fecTests.Run(new ParityTests()); diff --git a/test/FastExpressionCompiler.TestsRunner/Program.cs b/test/FastExpressionCompiler.TestsRunner/Program.cs index 02ec4ed1..d0f60382 100644 --- a/test/FastExpressionCompiler.TestsRunner/Program.cs +++ b/test/FastExpressionCompiler.TestsRunner/Program.cs @@ -76,6 +76,7 @@ public static void Main(string[] args) lt.Run(new LightExpression.IssueTests.Issue499_InvalidProgramException_for_Sorting_and_comparison_function()); lt.Run(new LightExpression.IssueTests.Issue561_NonBooleanEqualityOperator()); lt.Run(new LightExpression.UnitTests.RuntimeTypeConstantRegressionTests()); + lt.Run(new LightExpression.UnitTests.ExtensionReductionIdentityRegressionTests()); lt.Run(new LightExpression.UnitTests.ParityTests()); ltTime = ltStopwatch.ElapsedMilliseconds; }); @@ -86,6 +87,7 @@ public static void Main(string[] args) st.Run(new ReductionProgressRegressionTests()); st.Run(new RuntimeTypeConstantRegressionTests()); st.Run(new CustomReducibleNodeRegressionTests()); + st.Run(new ExtensionReductionIdentityRegressionTests()); st.Run(new ParityTests()); st.Run(new Issue498_InvalidProgramException_when_using_loop()); st.Run(new Issue495_Incomplete_pattern_detection_for_NotSupported_1007_Return_goto_from_TryCatch_with_Assign_generates_invalid_IL()); diff --git a/test/FastExpressionCompiler.UnitTests/ExtensionReductionIdentityRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/ExtensionReductionIdentityRegressionTests.cs new file mode 100644 index 00000000..76a526af --- /dev/null +++ b/test/FastExpressionCompiler.UnitTests/ExtensionReductionIdentityRegressionTests.cs @@ -0,0 +1,210 @@ +using System; +using System.Reflection.Emit; +#if LIGHT_EXPRESSION +using E = FastExpressionCompiler.LightExpression.Expression; +namespace FastExpressionCompiler.LightExpression.UnitTests; +#else +using E = System.Linq.Expressions.Expression; +namespace FastExpressionCompiler.UnitTests; +#endif + +public struct ExtensionReductionIdentityRegressionTests : ITestX +{ + private static readonly CompilerFlags[] Flags = + { + CompilerFlags.Default, CompilerFlags.NoInvocationLambdaInlining, + CompilerFlags.EnableDelegateDebugInfo | CompilerFlags.ThrowOnNotSupportedExpression + }; + + public void Run(TestRun t) + { + Fresh_blocks_and_captured_constants_share_the_collected_tree(t); + Fresh_nested_lambdas_share_the_collected_tree(t); + Fresh_labels_share_the_collected_tree(t); + Shared_and_distinct_nodes_use_reference_identity(t); + Reductions_are_local_to_each_compilation(t); + Public_emission_paths_reduce_once(t); + Invalid_first_reductions_remain_rejected(t); +#if LIGHT_EXPRESSION + Intrinsic_nodes_keep_their_collection_and_emission_hooks(t); +#endif + } + + public void Fresh_blocks_and_captured_constants_share_the_collected_tree(TestContext t) + { + foreach (var flags in Flags) + { + var node = IntBlock(41); + var fast = E.Lambda>(node).CompileFast(true, flags); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()); + t.AreEqual(1, node.Reductions); + } + } + + public void Fresh_nested_lambdas_share_the_collected_tree(TestContext t) + { + foreach (var flags in Flags) + { + var node = new FreshReduction(typeof(Func), () => + { + var local = E.Variable(typeof(int), "fresh"); + return E.Block(new[] { local }, E.Assign(local, E.Constant(41)), + E.Lambda>(E.Add(local, E.Constant(1)))); + }); + var fast = E.Lambda>>(node).CompileFast(true, flags); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()()); + t.AreEqual(1, node.Reductions); + } + } + + public void Fresh_labels_share_the_collected_tree(TestContext t) + { + FreshReduction Node() => new FreshReduction(typeof(int), () => + { + var target = E.Label(typeof(int), "fresh"); + return E.Block(E.Goto(target, E.Constant(42)), E.Label(target, E.Constant(0))); + }); + foreach (var flags in Flags) + { + var node = Node(); + var fast = E.Lambda>(node).CompileFast(true, flags); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()); + t.AreEqual(1, node.Reductions); + var shared = Node(); + var repeated = E.Lambda>(E.Add(shared, shared)).CompileFast(true, flags); + if (t.IsNotNull(repeated)) t.AreEqual(84, repeated()); + t.AreEqual(1, shared.Reductions); + } + } + + public void Shared_and_distinct_nodes_use_reference_identity(TestContext t) + { + foreach (var flags in Flags) + { + var shared = IntBlock(20); + var repeated = E.Lambda>(E.Add(shared, shared)).CompileFast(true, flags); + if (t.IsNotNull(repeated)) t.AreEqual(42, repeated()); + t.AreEqual(1, shared.Reductions); + var left = IntBlock(20); + var right = IntBlock(21); + var distinct = E.Lambda>(E.Add(left, right)).CompileFast(true, flags); + if (t.IsNotNull(distinct)) t.AreEqual(43, distinct()); + t.AreEqual(1, left.Reductions); + t.AreEqual(1, right.Reductions); + var condition = E.Parameter(typeof(bool)); + var branches = E.Lambda>(E.Condition(condition, left, right), condition).CompileFast(true, flags); + if (t.IsNotNull(branches)) { t.AreEqual(21, branches(true)); t.AreEqual(22, branches(false)); } + t.AreEqual(2, left.Reductions); + t.AreEqual(2, right.Reductions); + } + } + + public void Reductions_are_local_to_each_compilation(TestContext t) + { + var node = new FreshReduction(typeof(Marker), () => E.Constant(new Marker { Value = 42 })); + var source = E.Lambda>(node); + var first = source.CompileFast(true); + var second = source.CompileFast(true); + if (t.IsNotNull(first) && t.IsNotNull(second)) + { + t.AreEqual(42, first().Value); + t.AreEqual(42, second().Value); + t.IsFalse(ReferenceEquals(first(), second())); + } + t.AreEqual(2, node.Reductions); + } + + public void Public_emission_paths_reduce_once(TestContext t) + { + FreshReduction Node() => new FreshReduction(typeof(int), () => + { + var local = E.Variable(typeof(int), "fresh"); + return E.Block(new[] { local }, E.Assign(local, E.Constant(42)), local); + }); + var direct = Node(); + var fast = E.Lambda>(direct).TryCompileWithoutClosure>(); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()); + t.AreEqual(1, direct.Reductions); + var emitted = Node(); + var method = new DynamicMethod("FreshExtension", typeof(int), Type.EmptyTypes); + if (t.IsTrue(E.Lambda>(emitted).CompileFastToIL(method.GetILGenerator()))) + t.AreEqual(42, ((Func)method.CreateDelegate(typeof(Func)))()); + t.AreEqual(1, emitted.Reductions); + } + + public void Invalid_first_reductions_remain_rejected(TestContext t) + { + foreach (var self in new[] { false, true }) + { + FreshReduction node = null; + node = new FreshReduction(typeof(int), () => self ? node : null); + t.IsNull(E.Lambda>(node).CompileFast(true)); + t.AreEqual(1, node.Reductions); + t.IsNull(E.Lambda>(node).TryCompileWithoutClosure>()); + t.AreEqual(2, node.Reductions); + } + } + + private static FreshReduction IntBlock(int value) => new FreshReduction(typeof(int), () => + { + var local = E.Variable(typeof(Marker), "fresh"); + return E.Block(new[] { local }, E.Assign(local, E.Constant(new Marker { Value = value })), + E.Add(E.Field(local, nameof(Marker.Value)), E.Constant(1))); + }); + + public sealed class Marker { public int Value; } + +#if LIGHT_EXPRESSION + private sealed class FreshReduction : UnaryExpression +#else + private sealed class FreshReduction : E +#endif + { + private readonly Func _reduce; + public int Reductions; + public override System.Linq.Expressions.ExpressionType NodeType => System.Linq.Expressions.ExpressionType.Extension; +#if LIGHT_EXPRESSION + public FreshReduction(Type type, Func reduce) : base(E.Default(type)) => _reduce = reduce; +#else + public override Type Type { get; } + public override bool CanReduce => true; + public FreshReduction(Type type, Func reduce) { Type = type; _reduce = reduce; } +#endif + public override E Reduce() { ++Reductions; return _reduce(); } + public override bool Equals(object obj) => obj is FreshReduction; + public override int GetHashCode() => 0; + } + +#if LIGHT_EXPRESSION + public void Intrinsic_nodes_keep_their_collection_and_emission_hooks(TestContext t) + { + var node = new Intrinsic(); + var fast = E.Lambda>(node).CompileFast(true); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()); + t.AreEqual(1, node.Collections); + t.AreEqual(1, node.Emissions); + } + + private sealed class Intrinsic : UnaryExpression + { + public int Collections, Emissions; + public Intrinsic() : base(E.Default(typeof(int))) { } + public override System.Linq.Expressions.ExpressionType NodeType => System.Linq.Expressions.ExpressionType.Extension; + public override bool IsIntrinsic => true; + public override E Reduce() => throw new InvalidOperationException("Intrinsic reduction should not be used."); + public override ExpressionCompiler.Result TryCollectInfo(ref ExpressionCompiler.CompilerContext context, + ExpressionCompiler.NestedLambdaInfo nestedLambda, + ref ImTools.SmallList rootNestedLambdas, short scopeIdx) + { + ++Collections; + return ExpressionCompiler.Result.OK; + } + public override bool TryEmit(ref ExpressionCompiler.CompilerContext context, ILGenerator il, ExpressionCompiler.ParentFlags parent, int byRefIndex = -1) + { + ++Emissions; + il.Emit(OpCodes.Ldc_I4, 42); + return true; + } + } +#endif +} diff --git a/test/FastExpressionCompiler.UnitTests/ReductionProgressRegressionTests.cs b/test/FastExpressionCompiler.UnitTests/ReductionProgressRegressionTests.cs index 28c10afd..024d3fae 100644 --- a/test/FastExpressionCompiler.UnitTests/ReductionProgressRegressionTests.cs +++ b/test/FastExpressionCompiler.UnitTests/ReductionProgressRegressionTests.cs @@ -9,7 +9,7 @@ public void Run(TestRun t) { Self_reductions_follow_the_strict_rejection_contract(t); Null_reductions_follow_the_strict_rejection_contract(t); - Emission_rejects_a_reduction_that_stops_progressing(t); + Emission_reuses_the_valid_collected_reduction(t); Valid_reduction_chains_keep_parameters(t); Compile_to_IL_rejects_nonprogressing_reductions(t); } @@ -32,11 +32,13 @@ public void Null_reductions_follow_the_strict_rejection_contract(TestContext t) .CompileFast(true, CompilerFlags.ThrowOnNotSupportedExpression)); t.Throws(() => Expression.Lambda>(new NullReduction()).CompileFast()); } - public void Emission_rejects_a_reduction_that_stops_progressing(TestContext t) + public void Emission_reuses_the_valid_collected_reduction(TestContext t) { var node = new BoundedSelf(true, firstValue: true); - t.IsNull(Expression.Lambda>(node).CompileFast(true)); - t.AreEqual(2, node.Count); + // Collection already supplied a valid tree; emission must reuse it. + var fast = Expression.Lambda>(node).CompileFast(true); + if (t.IsNotNull(fast)) t.AreEqual(42, fast()); + t.AreEqual(1, node.Count); } public void Valid_reduction_chains_keep_parameters(TestContext t) {