Skip to content

Commit 1ebd6e2

Browse files
Copilotdadhi
andauthored
test: add deterministic flat expression property checks
Agent-Logs-Url: https://github.com/dadhi/FastExpressionCompiler/sessions/b03ffb96-068c-4255-bd58-3ca2f3ef1298 Co-authored-by: dadhi <39516+dadhi@users.noreply.github.com>
1 parent f4bd9e9 commit 1ebd6e2

3 files changed

Lines changed: 321 additions & 2 deletions

File tree

test/FastExpressionCompiler.LightExpression.UnitTests/FastExpressionCompiler.LightExpression.UnitTests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@
1919
<Reference Include="Microsoft.CSharp" />
2020
</ItemGroup>
2121

22+
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0' OR '$(TargetFramework)' == 'net9.0'">
23+
<PackageReference Include="CsCheck" Version="4.6.2" />
24+
</ItemGroup>
25+
2226
</Project>
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq.Expressions;
4+
using FastExpressionCompiler.FlatExpression;
5+
using static FastExpressionCompiler.LightExpression.Expression;
6+
#if NET8_0_OR_GREATER
7+
using CsCheck;
8+
#endif
9+
10+
namespace FastExpressionCompiler.LightExpression.UnitTests;
11+
12+
public partial class LightExpressionTests
13+
{
14+
#if NET8_0_OR_GREATER
15+
public void Can_property_test_generated_flat_expression_roundtrip_structurally()
16+
{
17+
Gen.Int[0, int.MaxValue]
18+
.Select(seed => new GeneratedCase(seed, GeneratedIntSpecFactory.Create(seed, maxDepth: 3, maxBreadth: 3)))
19+
.Sample(testCase =>
20+
GeneratedExpressionComparer.AreEqual(
21+
CreateGeneratedLightExpression(testCase.Spec),
22+
CreateGeneratedFlatExpression(testCase.Spec).ToLightExpression()),
23+
iter: 100,
24+
threads: 1,
25+
seed: "0N0XIzNsQ0O2",
26+
print: testCase => $"{testCase.Seed}: {testCase.Spec}");
27+
}
28+
29+
private static FastExpressionCompiler.LightExpression.Expression<Func<int, int>> CreateGeneratedLightExpression(IntSpec spec)
30+
{
31+
var parameter = ParameterOf<int>("p");
32+
return Lambda<Func<int, int>>(BuildLightInt(spec, [parameter]), parameter);
33+
}
34+
35+
private static ExprTree CreateGeneratedFlatExpression(IntSpec spec)
36+
{
37+
var fe = default(ExprTree);
38+
var parameter = fe.ParameterOf<int>("p");
39+
fe.RootIndex = fe.Lambda<Func<int, int>>(BuildFlatInt(ref fe, spec, [parameter]), parameter);
40+
return fe;
41+
}
42+
43+
private static FastExpressionCompiler.LightExpression.Expression BuildLightInt(IntSpec spec, FastExpressionCompiler.LightExpression.ParameterExpression[] ints) =>
44+
spec switch
45+
{
46+
IntSpec.ParameterRef parameter => ints[parameter.Index],
47+
IntSpec.Constant constant => Constant(constant.Value),
48+
IntSpec.Add add => Add(BuildLightInt(add.Left, ints), BuildLightInt(add.Right, ints)),
49+
IntSpec.Subtract subtract => Subtract(BuildLightInt(subtract.Left, ints), BuildLightInt(subtract.Right, ints)),
50+
IntSpec.Multiply multiply => Multiply(BuildLightInt(multiply.Left, ints), BuildLightInt(multiply.Right, ints)),
51+
IntSpec.Conditional conditional => Condition(
52+
BuildLightBool(conditional.Test, ints),
53+
BuildLightInt(conditional.IfTrue, ints),
54+
BuildLightInt(conditional.IfFalse, ints)),
55+
IntSpec.LetMany letMany => BuildLightBlock(letMany, ints),
56+
_ => throw new NotSupportedException(spec.GetType().Name)
57+
};
58+
59+
private static int BuildFlatInt(ref ExprTree fe, IntSpec spec, int[] ints) =>
60+
spec switch
61+
{
62+
IntSpec.ParameterRef parameter => ints[parameter.Index],
63+
IntSpec.Constant constant => fe.ConstantInt(constant.Value),
64+
IntSpec.Add add => fe.Add(BuildFlatInt(ref fe, add.Left, ints), BuildFlatInt(ref fe, add.Right, ints)),
65+
IntSpec.Subtract subtract => fe.MakeBinary(ExpressionType.Subtract,
66+
BuildFlatInt(ref fe, subtract.Left, ints), BuildFlatInt(ref fe, subtract.Right, ints)),
67+
IntSpec.Multiply multiply => fe.MakeBinary(ExpressionType.Multiply,
68+
BuildFlatInt(ref fe, multiply.Left, ints), BuildFlatInt(ref fe, multiply.Right, ints)),
69+
IntSpec.Conditional conditional => fe.Condition(
70+
BuildFlatBool(ref fe, conditional.Test, ints),
71+
BuildFlatInt(ref fe, conditional.IfTrue, ints),
72+
BuildFlatInt(ref fe, conditional.IfFalse, ints)),
73+
IntSpec.LetMany letMany => BuildFlatBlock(ref fe, letMany, ints),
74+
_ => throw new NotSupportedException(spec.GetType().Name)
75+
};
76+
77+
private static FastExpressionCompiler.LightExpression.Expression BuildLightBool(BoolSpec spec, FastExpressionCompiler.LightExpression.ParameterExpression[] ints) =>
78+
spec switch
79+
{
80+
BoolSpec.Constant constant => Constant(constant.Value),
81+
BoolSpec.Not not => Not(BuildLightBool(not.Operand, ints)),
82+
BoolSpec.Equal equal => Equal(BuildLightInt(equal.Left, ints), BuildLightInt(equal.Right, ints)),
83+
BoolSpec.GreaterThan greaterThan => GreaterThan(BuildLightInt(greaterThan.Left, ints), BuildLightInt(greaterThan.Right, ints)),
84+
BoolSpec.AndAlso andAlso => AndAlso(BuildLightBool(andAlso.Left, ints), BuildLightBool(andAlso.Right, ints)),
85+
BoolSpec.OrElse orElse => OrElse(BuildLightBool(orElse.Left, ints), BuildLightBool(orElse.Right, ints)),
86+
_ => throw new NotSupportedException(spec.GetType().Name)
87+
};
88+
89+
private static int BuildFlatBool(ref ExprTree fe, BoolSpec spec, int[] ints) =>
90+
spec switch
91+
{
92+
BoolSpec.Constant constant => fe.ConstantOf(constant.Value),
93+
BoolSpec.Not not => fe.Not(BuildFlatBool(ref fe, not.Operand, ints)),
94+
BoolSpec.Equal equal => fe.Equal(BuildFlatInt(ref fe, equal.Left, ints), BuildFlatInt(ref fe, equal.Right, ints)),
95+
BoolSpec.GreaterThan greaterThan => fe.MakeBinary(ExpressionType.GreaterThan,
96+
BuildFlatInt(ref fe, greaterThan.Left, ints), BuildFlatInt(ref fe, greaterThan.Right, ints)),
97+
BoolSpec.AndAlso andAlso => fe.MakeBinary(ExpressionType.AndAlso,
98+
BuildFlatBool(ref fe, andAlso.Left, ints), BuildFlatBool(ref fe, andAlso.Right, ints)),
99+
BoolSpec.OrElse orElse => fe.MakeBinary(ExpressionType.OrElse,
100+
BuildFlatBool(ref fe, orElse.Left, ints), BuildFlatBool(ref fe, orElse.Right, ints)),
101+
_ => throw new NotSupportedException(spec.GetType().Name)
102+
};
103+
104+
private static FastExpressionCompiler.LightExpression.Expression BuildLightBlock(IntSpec.LetMany letMany, FastExpressionCompiler.LightExpression.ParameterExpression[] ints)
105+
{
106+
var locals = new FastExpressionCompiler.LightExpression.ParameterExpression[letMany.Values.Length];
107+
var expressions = new FastExpressionCompiler.LightExpression.Expression[letMany.Values.Length + 1];
108+
for (var i = 0; i < locals.Length; ++i)
109+
{
110+
locals[i] = Variable(typeof(int), "v" + i);
111+
expressions[i] = Assign(locals[i], BuildLightInt(letMany.Values[i], ints));
112+
}
113+
114+
expressions[locals.Length] = BuildLightInt(letMany.Body, Append(ints, locals));
115+
return Block(locals, expressions);
116+
}
117+
118+
private static int BuildFlatBlock(ref ExprTree fe, IntSpec.LetMany letMany, int[] ints)
119+
{
120+
var locals = new int[letMany.Values.Length];
121+
var expressions = new int[letMany.Values.Length + 1];
122+
for (var i = 0; i < locals.Length; ++i)
123+
{
124+
locals[i] = fe.Variable(typeof(int), "v" + i);
125+
expressions[i] = fe.Assign(locals[i], BuildFlatInt(ref fe, letMany.Values[i], ints));
126+
}
127+
128+
expressions[locals.Length] = BuildFlatInt(ref fe, letMany.Body, Append(ints, locals));
129+
return fe.Block(typeof(int), locals, expressions);
130+
}
131+
132+
private static T[] Append<T>(T[] source, T[] append)
133+
{
134+
var result = new T[source.Length + append.Length];
135+
Array.Copy(source, result, source.Length);
136+
Array.Copy(append, 0, result, source.Length, append.Length);
137+
return result;
138+
}
139+
140+
private readonly record struct GeneratedCase(int Seed, IntSpec Spec);
141+
142+
private abstract record IntSpec
143+
{
144+
public sealed record ParameterRef(int Index) : IntSpec;
145+
public sealed record Constant(int Value) : IntSpec;
146+
public sealed record Add(IntSpec Left, IntSpec Right) : IntSpec;
147+
public sealed record Subtract(IntSpec Left, IntSpec Right) : IntSpec;
148+
public sealed record Multiply(IntSpec Left, IntSpec Right) : IntSpec;
149+
public sealed record Conditional(BoolSpec Test, IntSpec IfTrue, IntSpec IfFalse) : IntSpec;
150+
public sealed record LetMany(IntSpec[] Values, IntSpec Body) : IntSpec;
151+
}
152+
153+
private abstract record BoolSpec
154+
{
155+
public sealed record Constant(bool Value) : BoolSpec;
156+
public sealed record Not(BoolSpec Operand) : BoolSpec;
157+
public sealed record Equal(IntSpec Left, IntSpec Right) : BoolSpec;
158+
public sealed record GreaterThan(IntSpec Left, IntSpec Right) : BoolSpec;
159+
public sealed record AndAlso(BoolSpec Left, BoolSpec Right) : BoolSpec;
160+
public sealed record OrElse(BoolSpec Left, BoolSpec Right) : BoolSpec;
161+
}
162+
163+
private static class GeneratedIntSpecFactory
164+
{
165+
public static IntSpec Create(int seed, int maxDepth, int maxBreadth) =>
166+
NextInt(new Random(seed), maxDepth, envIntCount: 1, maxBreadth);
167+
168+
private static IntSpec NextInt(Random random, int depth, int envIntCount, int maxBreadth)
169+
{
170+
if (depth <= 0)
171+
return NextIntLeaf(random, envIntCount);
172+
173+
switch (random.Next(7))
174+
{
175+
case 0: return NextIntLeaf(random, envIntCount);
176+
case 1: return new IntSpec.Add(NextInt(random, depth - 1, envIntCount, maxBreadth), NextInt(random, depth - 1, envIntCount, maxBreadth));
177+
case 2: return new IntSpec.Subtract(NextInt(random, depth - 1, envIntCount, maxBreadth), NextInt(random, depth - 1, envIntCount, maxBreadth));
178+
case 3: return new IntSpec.Multiply(NextInt(random, depth - 1, envIntCount, maxBreadth), NextInt(random, depth - 1, envIntCount, maxBreadth));
179+
case 4: return new IntSpec.Conditional(
180+
NextBool(random, depth - 1, envIntCount, maxBreadth),
181+
NextInt(random, depth - 1, envIntCount, maxBreadth),
182+
NextInt(random, depth - 1, envIntCount, maxBreadth));
183+
case 5: return NextLetMany(random, depth - 1, envIntCount, maxBreadth);
184+
default: return new IntSpec.Constant(random.Next(-8, 9));
185+
}
186+
}
187+
188+
private static IntSpec NextIntLeaf(Random random, int envIntCount) =>
189+
random.Next(3) == 0
190+
? new IntSpec.Constant(random.Next(-8, 9))
191+
: new IntSpec.ParameterRef(random.Next(envIntCount));
192+
193+
private static IntSpec NextLetMany(Random random, int depth, int envIntCount, int maxBreadth)
194+
{
195+
var count = random.Next(1, maxBreadth + 1);
196+
var values = new IntSpec[count];
197+
for (var i = 0; i < count; ++i)
198+
values[i] = NextInt(random, depth, envIntCount, maxBreadth);
199+
return new IntSpec.LetMany(values, NextInt(random, depth, envIntCount + count, maxBreadth));
200+
}
201+
202+
private static BoolSpec NextBool(Random random, int depth, int envIntCount, int maxBreadth)
203+
{
204+
if (depth <= 0)
205+
return NextBoolLeaf(random, envIntCount);
206+
207+
switch (random.Next(6))
208+
{
209+
case 0: return NextBoolLeaf(random, envIntCount);
210+
case 1: return new BoolSpec.Not(NextBool(random, depth - 1, envIntCount, maxBreadth));
211+
case 2: return new BoolSpec.Equal(NextInt(random, depth - 1, envIntCount, maxBreadth), NextInt(random, depth - 1, envIntCount, maxBreadth));
212+
case 3: return new BoolSpec.GreaterThan(NextInt(random, depth - 1, envIntCount, maxBreadth), NextInt(random, depth - 1, envIntCount, maxBreadth));
213+
case 4: return new BoolSpec.AndAlso(NextBool(random, depth - 1, envIntCount, maxBreadth), NextBool(random, depth - 1, envIntCount, maxBreadth));
214+
default: return new BoolSpec.OrElse(NextBool(random, depth - 1, envIntCount, maxBreadth), NextBool(random, depth - 1, envIntCount, maxBreadth));
215+
}
216+
}
217+
218+
private static BoolSpec NextBoolLeaf(Random random, int envIntCount) =>
219+
random.Next(2) == 0
220+
? new BoolSpec.Constant(random.Next(2) == 0)
221+
: new BoolSpec.Equal(NextIntLeaf(random, envIntCount), NextIntLeaf(random, envIntCount));
222+
}
223+
224+
private sealed class GeneratedExpressionComparer
225+
{
226+
private readonly List<FastExpressionCompiler.LightExpression.ParameterExpression> _xs = new();
227+
private readonly List<FastExpressionCompiler.LightExpression.ParameterExpression> _ys = new();
228+
229+
public static bool AreEqual(FastExpressionCompiler.LightExpression.Expression x, FastExpressionCompiler.LightExpression.Expression y) => new GeneratedExpressionComparer().Eq(x, y);
230+
231+
private bool Eq(FastExpressionCompiler.LightExpression.Expression x, FastExpressionCompiler.LightExpression.Expression y)
232+
{
233+
if (ReferenceEquals(x, y))
234+
return true;
235+
if (x == null || y == null || x.NodeType != y.NodeType || x.Type != y.Type)
236+
return false;
237+
238+
return x.NodeType switch
239+
{
240+
ExpressionType.Lambda => EqLambda((FastExpressionCompiler.LightExpression.LambdaExpression)x, (FastExpressionCompiler.LightExpression.LambdaExpression)y),
241+
ExpressionType.Parameter => EqParameter((FastExpressionCompiler.LightExpression.ParameterExpression)x, (FastExpressionCompiler.LightExpression.ParameterExpression)y),
242+
ExpressionType.Constant => Equals(((FastExpressionCompiler.LightExpression.ConstantExpression)x).Value, ((FastExpressionCompiler.LightExpression.ConstantExpression)y).Value),
243+
ExpressionType.Not => Eq(((FastExpressionCompiler.LightExpression.UnaryExpression)x).Operand, ((FastExpressionCompiler.LightExpression.UnaryExpression)y).Operand),
244+
ExpressionType.Add or ExpressionType.Subtract or ExpressionType.Multiply or ExpressionType.Assign
245+
or ExpressionType.Equal or ExpressionType.GreaterThan or ExpressionType.AndAlso or ExpressionType.OrElse
246+
=> EqBinary((FastExpressionCompiler.LightExpression.BinaryExpression)x, (FastExpressionCompiler.LightExpression.BinaryExpression)y),
247+
ExpressionType.Conditional => EqConditional((FastExpressionCompiler.LightExpression.ConditionalExpression)x, (FastExpressionCompiler.LightExpression.ConditionalExpression)y),
248+
ExpressionType.Block => EqBlock((FastExpressionCompiler.LightExpression.BlockExpression)x, (FastExpressionCompiler.LightExpression.BlockExpression)y),
249+
_ => throw new NotSupportedException(x.NodeType.ToString())
250+
};
251+
}
252+
253+
private bool EqLambda(FastExpressionCompiler.LightExpression.LambdaExpression x, FastExpressionCompiler.LightExpression.LambdaExpression y)
254+
{
255+
if (x.Parameters.Count != y.Parameters.Count)
256+
return false;
257+
258+
var start = _xs.Count;
259+
for (var i = 0; i < x.Parameters.Count; ++i)
260+
{
261+
_xs.Add(x.Parameters[i]);
262+
_ys.Add(y.Parameters[i]);
263+
}
264+
265+
var equal = Eq(x.Body, y.Body);
266+
_xs.RemoveRange(start, _xs.Count - start);
267+
_ys.RemoveRange(start, _ys.Count - start);
268+
return equal;
269+
}
270+
271+
private bool EqParameter(FastExpressionCompiler.LightExpression.ParameterExpression x, FastExpressionCompiler.LightExpression.ParameterExpression y)
272+
{
273+
for (var i = _xs.Count - 1; i >= 0; --i)
274+
{
275+
var xMatches = ReferenceEquals(_xs[i], x);
276+
var yMatches = ReferenceEquals(_ys[i], y);
277+
if (xMatches || yMatches)
278+
return xMatches && yMatches;
279+
}
280+
281+
return x.Name == y.Name;
282+
}
283+
284+
private bool EqBinary(FastExpressionCompiler.LightExpression.BinaryExpression x, FastExpressionCompiler.LightExpression.BinaryExpression y) =>
285+
x.Method == y.Method && Eq(x.Left, y.Left) && Eq(x.Right, y.Right);
286+
287+
private bool EqConditional(FastExpressionCompiler.LightExpression.ConditionalExpression x, FastExpressionCompiler.LightExpression.ConditionalExpression y) =>
288+
Eq(x.Test, y.Test) && Eq(x.IfTrue, y.IfTrue) && Eq(x.IfFalse, y.IfFalse);
289+
290+
private bool EqBlock(FastExpressionCompiler.LightExpression.BlockExpression x, FastExpressionCompiler.LightExpression.BlockExpression y)
291+
{
292+
if (x.Variables.Count != y.Variables.Count || x.Expressions.Count != y.Expressions.Count)
293+
return false;
294+
295+
var start = _xs.Count;
296+
for (var i = 0; i < x.Variables.Count; ++i)
297+
{
298+
_xs.Add(x.Variables[i]);
299+
_ys.Add(y.Variables[i]);
300+
}
301+
302+
var equal = true;
303+
for (var i = 0; equal && i < x.Expressions.Count; ++i)
304+
equal = Eq(x.Expressions[i], y.Expressions[i]);
305+
306+
_xs.RemoveRange(start, _xs.Count - start);
307+
_ys.RemoveRange(start, _ys.Count - start);
308+
return equal;
309+
}
310+
}
311+
#else
312+
public void Can_property_test_generated_flat_expression_roundtrip_structurally() { }
313+
#endif
314+
}

test/FastExpressionCompiler.LightExpression.UnitTests/LightExpressionTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
namespace FastExpressionCompiler.LightExpression.UnitTests
1313
{
1414

15-
public class LightExpressionTests : ITest
15+
public partial class LightExpressionTests : ITest
1616
{
1717
public int Run()
1818
{
@@ -33,7 +33,8 @@ public int Run()
3333
Can_convert_dynamic_runtime_variables_and_debug_info_to_light_expression_and_flat_expression();
3434
Can_build_flat_expression_directly_with_light_expression_like_api();
3535
Can_build_flat_expression_control_flow_directly();
36-
return 16;
36+
Can_property_test_generated_flat_expression_roundtrip_structurally();
37+
return 17;
3738
}
3839

3940

0 commit comments

Comments
 (0)