Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 85 additions & 37 deletions src/FastExpressionCompiler/FastExpressionCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2423,45 +2423,11 @@ public static bool TryEmit(Expression expr, ILGenerator il, ref CompilerContext
logical.Left.Type == typeof(bool?) && logical.Right.Type == typeof(bool?))
return TryEmitNullableBooleanShortCircuit(logical.Left, logical.Right, nodeType == ExpressionType.AndAlso, il, ref context, parent);

var logicalMethod = logical.Method;
if (logicalMethod != null && !exprType.IsPrimitive)
{
// User-defined `&&` and `||`: `op_False(left) ? left : method(left, right)`, and `op_True` for the `||`.
// The right is emitted with the empty stack on purpose, so it may contain the try or the other non-stack-safe expression.
var leftExpr = logical.Left;
var isAndAlso = nodeType == ExpressionType.AndAlso;
var leftType = leftExpr.Type;
var shortCircuitMethod = leftType.GetMethod(isAndAlso ? "op_False" : "op_True", BindingFlags.Public | BindingFlags.Static, null, new[] { leftType }, null);
if (shortCircuitMethod == null)
return false;

var operandParent = parent & ~ParentFlags.IgnoreResult;
if (!TryEmit(leftExpr, il, ref context, operandParent))
return false;
var leftVar = EmitStoreLocalVariable(il, leftType);
EmitLoadLocalVariable(il, leftVar);
if (!EmitMethodCall(il, shortCircuitMethod))
return false;
var skipRightLabel = il.DefineLabel();
il.Demit(OpCodes.Brtrue, skipRightLabel);

if (!TryEmit(logical.Right, il, ref context, operandParent))
return false;
var rightVar = EmitStoreLocalVariable(il, logical.Right.Type);
EmitLoadLocalVariable(il, leftVar);
EmitLoadLocalVariable(il, rightVar);
if (!EmitMethodCall(il, logicalMethod))
return false;
var doneLabel = il.DefineLabel();
il.Demit(OpCodes.Br, doneLabel);

il.DmarkLabel(skipRightLabel);
EmitLoadLocalVariable(il, leftVar);
il.DmarkLabel(doneLabel);
return il.EmitPopIfIgnoreResult(parent);
}
if (!exprType.IsPrimitive)
{
var isLifted = logical.IsLifted;
if (isLifted || logical.Method != null)
return TryEmitUserDefinedLogicalOperator(logical, nodeType, isLifted, il, ref context, parent);
Debug.WriteLine("Unsupported: non-primitive result in || or && (other than `bool?` operands without a method), see #480: " + expr);
return false;
}
Expand Down Expand Up @@ -7304,6 +7270,88 @@ private static bool TryEmitArithmeticOperation(Type leftType, Type rightType, Ex
return true;
}

private static bool TryEmitUserDefinedLogicalOperator(BinaryExpression expr, ExpressionType nodeType, bool isLifted,
ILGenerator il, ref CompilerContext context, ParentFlags parent)
{
var type = expr.Left.Type;
var operandType = isLifted ? Nullable.GetUnderlyingType(type) : type;
var method = expr.Method;
if (isLifted && method == null)
method = FindBinaryOperandMethod(
nodeType == ExpressionType.AndAlso ? "op_BitwiseAnd" : "op_BitwiseOr", operandType, operandType,
Nullable.GetUnderlyingType(expr.Right.Type) ?? expr.Right.Type, operandType);
if (operandType == null || method == null || isLifted && method.ContainsGenericParameters)
return false;
var testType = isLifted ? method.DeclaringType : type;
var bindingFlags = BindingFlags.Public | BindingFlags.Static;
if (isLifted)
bindingFlags |= BindingFlags.FlattenHierarchy;
var test = testType.GetMethod(nodeType == ExpressionType.AndAlso ? "op_False" : "op_True",
bindingFlags, null, new[] { operandType }, null);
if (test == null || isLifted && test.ReturnType != typeof(bool))
return false;

var flags = isLifted
? (parent & ~(ParentFlags.IgnoreResult | ParentFlags.InstanceAccess | ParentFlags.MemberAccess |
ParentFlags.LambdaCall | ParentFlags.ReturnByRef)) | ParentFlags.AssignmentRightValue
: parent & ~ParentFlags.IgnoreResult;
if (!TryEmit(expr.Left, il, ref context, flags))
return false;
var left = EmitStoreLocalVariable(il, type);
var skipRight = il.DefineLabel();
if (isLifted)
{
EmitLoadLocalVariableAddress(il, left);
EmitMethodCall(il, type.GetNullableHasValueGetterMethod());
il.Demit(OpCodes.Brfalse, skipRight);
EmitLoadLocalVariableAddress(il, left);
il.Demit(OpCodes.Ldfld, type.GetNullableValueUnsafeAkaGetValueOrDefaultMethod());
}
else
EmitLoadLocalVariable(il, left);
EmitMethodCall(il, test);
il.Demit(OpCodes.Brtrue, skipRight);

// Both paths evaluate the right on an empty stack, including exception regions.
if (!TryEmit(expr.Right, il, ref context, flags))
return false;
var right = EmitStoreLocalVariable(il, expr.Right.Type);
var rightIsNull = default(Label);
if (isLifted)
{
rightIsNull = il.DefineLabel();
EmitLoadLocalVariableAddress(il, right);
EmitMethodCall(il, type.GetNullableHasValueGetterMethod());
il.Demit(OpCodes.Brfalse, rightIsNull);
EmitLoadLocalVariableAddress(il, left);
il.Demit(OpCodes.Ldfld, type.GetNullableValueUnsafeAkaGetValueOrDefaultMethod());
EmitLoadLocalVariableAddress(il, right);
il.Demit(OpCodes.Ldfld, type.GetNullableValueUnsafeAkaGetValueOrDefaultMethod());
}
else
{
EmitLoadLocalVariable(il, left);
EmitLoadLocalVariable(il, right);
}
EmitMethodCall(il, method);
if (isLifted)
il.Demit(OpCodes.Newobj, type.GetNullableConstructor());
var done = il.DefineLabel();
il.Demit(OpCodes.Br, done);
il.DmarkLabel(skipRight);
EmitLoadLocalVariable(il, left);
if (isLifted)
{
il.Demit(OpCodes.Br, done);
il.DmarkLabel(rightIsNull);
EmitDefault(il, type);
}
il.DmarkLabel(done);
if (isLifted)
context.LastEmitIsAddress = false;
return il.EmitPopIfIgnoreResult(parent);
}

private static bool TryEmitLogicalOperator(BinaryExpression expr, ExpressionType nodeType,
ILGenerator il, ref CompilerContext context, ParentFlags parent)
{
Expand Down
4 changes: 4 additions & 0 deletions test/FastExpressionCompiler.TestsRunner.Net472/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ static void RunLightExpressionTests(object state)
var t = (LightExpression.TestRun)state;
t.Run(new LightExpression.UnitTests.VoidSwitchConversionRegressionTests());
t.Run(new LightExpression.UnitTests.ReadonlyRefFieldRegressionTests());
t.Run(new LightExpression.UnitTests.LiftedUserDefinedLogicalRegressionTests());
t.Run(new LightExpression.UnitTests.UserDefinedLogicalRegressionTests());
t.Run(new LightExpression.UnitTests.VoidLabelValueRegressionTests());
t.Run(new LightExpression.UnitTests.ReferenceInstanceCallRegressionTests());
t.Run(new LightExpression.UnitTests.MultiRankArrayReceiverRegressionTests());
Expand Down Expand Up @@ -72,6 +74,8 @@ static void RunLightExpressionTests(object state)

var fecTests = new TestRun();
fecTests.Run(new ReadonlyRefFieldRegressionTests());
fecTests.Run(new LiftedUserDefinedLogicalRegressionTests());
fecTests.Run(new UserDefinedLogicalRegressionTests());
fecTests.Run(new VoidLabelValueRegressionTests());
fecTests.Run(new ReferenceInstanceCallRegressionTests());
fecTests.Run(new MultiRankArrayReceiverRegressionTests());
Expand Down
4 changes: 4 additions & 0 deletions test/FastExpressionCompiler.TestsRunner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public static void Main(string[] args)
var ltStopwatch = Stopwatch.StartNew();
lt.Run(new LightExpression.UnitTests.VoidSwitchConversionRegressionTests());
lt.Run(new LightExpression.UnitTests.ReadonlyRefFieldRegressionTests());
lt.Run(new LightExpression.UnitTests.LiftedUserDefinedLogicalRegressionTests());
lt.Run(new LightExpression.UnitTests.UserDefinedLogicalRegressionTests());
lt.Run(new LightExpression.UnitTests.VoidLabelValueRegressionTests());
lt.Run(new LightExpression.UnitTests.ReferenceInstanceCallRegressionTests());
lt.Run(new LightExpression.UnitTests.MultiRankArrayReceiverRegressionTests());
Expand Down Expand Up @@ -98,6 +100,8 @@ public static void Main(string[] args)

var stStopwatch = Stopwatch.StartNew();
st.Run(new ReadonlyRefFieldRegressionTests());
st.Run(new LiftedUserDefinedLogicalRegressionTests());
st.Run(new UserDefinedLogicalRegressionTests());
st.Run(new VoidLabelValueRegressionTests());
st.Run(new ReferenceInstanceCallRegressionTests());
st.Run(new MultiRankArrayReceiverRegressionTests());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using SysExpr = System.Linq.Expressions.Expression;
#if LIGHT_EXPRESSION
using Expr = FastExpressionCompiler.LightExpression.Expression;
namespace FastExpressionCompiler.LightExpression.UnitTests;
#else
using Expr = System.Linq.Expressions.Expression;
namespace FastExpressionCompiler.UnitTests;
#endif
public struct LiftedUserDefinedLogicalRegressionTests : ITestX
{
public void Run(TestRun t)
{
Lifted_conditional_operators_match_values_and_effects(t);
Lifted_explicit_methods_match_values_and_effects(t);
Ignored_lifted_conditional_operators_preserve_effects(t);
Lifted_right_operand_may_contain_finally(t);
Direct_factory_discovers_lifted_operator(t);
}
public void Lifted_conditional_operators_match_values_and_effects(TestContext t) => Check(t, false, false, false);
public void Lifted_explicit_methods_match_values_and_effects(TestContext t) => Check(t, true, false, false);
public void Ignored_lifted_conditional_operators_preserve_effects(TestContext t) => Check(t, false, true, false);
public void Lifted_right_operand_may_contain_finally(TestContext t) => Check(t, false, false, true);
private static void Check(TestContext t, bool explicitMethod, bool ignored, bool finallyEffect)
{
foreach (var and in new[] { false, true })
{
var left = SysExpr.Parameter(typeof(Func<Logical?>));
var right = SysExpr.Parameter(typeof(Func<Logical?>));
var method = explicitMethod ? typeof(Logical).GetMethod(and ? nameof(Logical.And) : nameof(Logical.Or)) : null;
SysExpr r = SysExpr.Invoke(right);
if (finallyEffect) r = SysExpr.TryFinally(r, SysExpr.Call(typeof(LiftedUserDefinedLogicalRegressionTests).GetMethod(nameof(Finally))));
var operation = and ? SysExpr.AndAlso(SysExpr.Invoke(left), r, method) : SysExpr.OrElse(SysExpr.Invoke(left), r, method);
var expression = ignored ? SysExpr.Lambda(SysExpr.Block(operation, SysExpr.Empty()), left, right) : SysExpr.Lambda(operation, left, right);
var fast = expression.FromSysExpression().CompileFast(true);
if (!t.IsNotNull(fast)) return;
var system = expression.Compile();
foreach (var a in new Logical?[] { null, new Logical { Value = 0 }, new Logical { Value = 3 } })
foreach (var b in new Logical?[] { null, new Logical { Value = 0 }, new Logical { Value = 3 } })
{
Func<Logical?> l = () => { Trace += "L;"; return a; };
Func<Logical?> rr = () => { Trace += "R;"; return b; };
Trace = "";
var expected = system.DynamicInvoke(l, rr);
var expectedTrace = Trace;
Trace = "";
t.AreEqual(expected, fast.DynamicInvoke(l, rr));
t.AreEqual(expectedTrace, Trace);
}
}
}
public void Direct_factory_discovers_lifted_operator(TestContext t)
{
var l = Expr.Parameter(typeof(Logical?));
var r = Expr.Parameter(typeof(Logical?));
foreach (var and in new[] { false, true })
{
var body = and ? Expr.AndAlso(l, r) : Expr.OrElse(l, r);
var fast = Expr.Lambda<Func<Logical?, Logical?, Logical?>>(body, l, r).CompileFast(true);
if (!t.IsNotNull(fast)) return;
t.AreEqual((Logical?)null, fast(null, new Logical { Value = 3 }));
t.AreEqual((Logical?)null, fast(new Logical { Value = and ? 3 : 0 }, null));
var expected = new Logical { Value = and ? 0 : 3 };
t.AreEqual((Logical?)expected, fast(expected, null));
}
}
public static string Trace;
public static void Finally() => Trace += "F;";
public struct Logical
{
public int Value;
public string Tag;
public static bool operator true(Logical value) { Trace += "T;"; return value.Value != 0; }
public static bool operator false(Logical value) { Trace += "F?;"; return value.Value == 0; }
public static Logical operator &(Logical left, Logical right) { Trace += "&;"; return new Logical { Value = left.Value + right.Value, Tag = "and" }; }
public static Logical operator |(Logical left, Logical right) { Trace += "|;"; return new Logical { Value = left.Value + right.Value, Tag = "or" }; }
public static Logical And(Logical left, Logical right) { Trace += "And;"; return new Logical { Value = left.Value + right.Value, Tag = "explicit and" }; }
public static Logical Or(Logical left, Logical right) { Trace += "Or;"; return new Logical { Value = left.Value + right.Value, Tag = "explicit or" }; }
}
}
Loading
Loading