diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs
index b903c1f5b..c79248324 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs
@@ -648,6 +648,21 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
Console.WriteLine(x() + a);
(new TS?[0])[0] += x();
}
+
+ public static bool RetEq(int? a, int? b)
+ {
+ return a == b;
+ }
+
+ public static bool RetLt(int? a, int? b)
+ {
+ return a < b;
+ }
+
+ public static bool RetNotLt(int? a, int? b)
+ {
+ return !(a < b);
+ }
}
// dummy structure for testing custom operators
diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
index 10bd1e886..ab6e97250 100644
--- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
@@ -423,6 +423,9 @@ namespace ICSharpCode.Decompiler.CSharp
protected internal override TranslatedExpression VisitComp(Comp inst, TranslationContext context)
{
+ if (inst.LiftingKind == ComparisonLiftingKind.ThreeValuedLogic) {
+ return ErrorExpression("Nullable comparisons with three-valued-logic not supported in C#");
+ }
if (inst.Kind.IsEqualityOrInequality()) {
bool negateOutput;
var result = TranslateCeq(inst, out negateOutput);
@@ -541,8 +544,12 @@ namespace ICSharpCode.Decompiler.CSharp
break;
}
if (inputType != KnownTypeCode.None) {
- left = left.ConvertTo(compilation.FindType(inputType), this);
- right = right.ConvertTo(compilation.FindType(inputType), this);
+ IType targetType = compilation.FindType(inputType);
+ if (inst.IsLifted) {
+ targetType = NullableType.Create(compilation, targetType);
+ }
+ left = left.ConvertTo(targetType, this);
+ right = right.ConvertTo(targetType, this);
}
var op = inst.Kind.ToBinaryOperatorType();
return new BinaryOperatorExpression(left.Expression, op, right.Expression)
diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs
index 825fcafdd..9f5ac3ad9 100644
--- a/ICSharpCode.Decompiler/IL/Instructions.cs
+++ b/ICSharpCode.Decompiler/IL/Instructions.cs
@@ -881,7 +881,7 @@ namespace ICSharpCode.Decompiler.IL
protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match)
{
var o = other as BinaryNumericInstruction;
- return o != null && this.Left.PerformMatch(o.Left, ref match) && this.Right.PerformMatch(o.Right, ref match) && CheckForOverflow == o.CheckForOverflow && Sign == o.Sign && Operator == o.Operator;
+ return o != null && this.Left.PerformMatch(o.Left, ref match) && this.Right.PerformMatch(o.Right, ref match) && CheckForOverflow == o.CheckForOverflow && Sign == o.Sign && Operator == o.Operator && IsLifted == o.IsLifted;
}
}
}
@@ -1757,7 +1757,7 @@ namespace ICSharpCode.Decompiler.IL
/// Comparison. The inputs must be both integers; or both floats; or both object references. Object references can only be compared for equality or inequality. Floating-point comparisons evaluate to 0 (false) when an input is NaN, except for 'NaN != NaN' which evaluates to 1 (true).
public sealed partial class Comp : BinaryInstruction
{
- public override StackType ResultType { get { return StackType.I4; } }
+
public override void AcceptVisitor(ILVisitor visitor)
{
visitor.VisitComp(this);
@@ -1773,7 +1773,7 @@ namespace ICSharpCode.Decompiler.IL
protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match)
{
var o = other as Comp;
- return o != null && this.Left.PerformMatch(o.Left, ref match) && this.Right.PerformMatch(o.Right, ref match) && this.Kind == o.Kind && this.Sign == o.Sign;
+ return o != null && this.Left.PerformMatch(o.Left, ref match) && this.Right.PerformMatch(o.Right, ref match) && this.Kind == o.Kind && this.Sign == o.Sign && this.LiftingKind == o.LiftingKind;
}
}
}
@@ -1881,7 +1881,7 @@ namespace ICSharpCode.Decompiler.IL
protected internal override bool PerformMatch(ILInstruction other, ref Patterns.Match match)
{
var o = other as Conv;
- return o != null && this.Argument.PerformMatch(o.Argument, ref match) && CheckForOverflow == o.CheckForOverflow && Kind == o.Kind && InputSign == o.InputSign && TargetType == o.TargetType;
+ return o != null && this.Argument.PerformMatch(o.Argument, ref match) && CheckForOverflow == o.CheckForOverflow && Kind == o.Kind && InputSign == o.InputSign && TargetType == o.TargetType && IsLifted == o.IsLifted;
}
}
}
diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt
index bc4a5b695..909f0f2ae 100644
--- a/ICSharpCode.Decompiler/IL/Instructions.tt
+++ b/ICSharpCode.Decompiler/IL/Instructions.tt
@@ -67,7 +67,7 @@
ResultType("I4"), Unary),
new OpCode("binary", "Common instruction for add, sub, mul, div, rem, bit.and, bit.or, bit.xor, shl and shr.",
CustomClassName("BinaryNumericInstruction"), Binary, CustomWriteTo, CustomConstructor, CustomComputeFlags,
- MatchCondition("CheckForOverflow == o.CheckForOverflow && Sign == o.Sign && Operator == o.Operator")),
+ MatchCondition("CheckForOverflow == o.CheckForOverflow && Sign == o.Sign && Operator == o.Operator && IsLifted == o.IsLifted")),
new OpCode("compound", "Common instruction for compound assignments.",
CustomClassName("CompoundAssignmentInstruction"), CustomConstructor, CustomComputeFlags,
MayThrow, CustomArguments("target", "value"), HasTypeOperand, ResultType("type.GetStackType()"), CustomWriteTo,
@@ -127,8 +127,8 @@
+ "Object references can only be compared for equality or inequality. "
+ "Floating-point comparisons evaluate to 0 (false) when an input is NaN, except for 'NaN != NaN' which "
+ "evaluates to 1 (true).",
- Binary, CustomConstructor, CustomWriteTo, ResultType("I4"),
- MatchCondition("this.Kind == o.Kind && this.Sign == o.Sign")),
+ Binary, CustomConstructor, CustomWriteTo,
+ MatchCondition("this.Kind == o.Kind && this.Sign == o.Sign && this.LiftingKind == o.LiftingKind")),
new OpCode("call", "Non-virtual method call.", Call),
new OpCode("callvirt", "Virtual method call.",
CustomClassName("CallVirt"), Call),
@@ -136,7 +136,7 @@
Unary, MayThrow, VoidResult),
new OpCode("conv", "Numeric cast.",
Unary, CustomConstructor,
- MatchCondition("CheckForOverflow == o.CheckForOverflow && Kind == o.Kind && InputSign == o.InputSign && TargetType == o.TargetType")),
+ MatchCondition("CheckForOverflow == o.CheckForOverflow && Kind == o.Kind && InputSign == o.InputSign && TargetType == o.TargetType && IsLifted == o.IsLifted")),
new OpCode("ldloc", "Loads the value of a local variable. (ldarg/ldloc)",
CustomClassName("LdLoc"), NoArguments, HasVariableOperand("Load"), ResultType("variable.StackType")),
new OpCode("ldloca", "Loads the address of a local variable. (ldarga/ldloca)",
diff --git a/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs
index 96d722d81..fb82af0ca 100644
--- a/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs
+++ b/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs
@@ -65,7 +65,7 @@ namespace ICSharpCode.Decompiler.IL
///
/// A lifted binary operation allows its arguments to be a value of type Nullable{T}, where
/// T.GetStackType() == [Left|Right]InputType.
- /// If both input values is non-null:
+ /// If both input values are non-null:
/// * they are sign/zero-extended to the corresponding InputType (based on T's sign)
/// * the underlying numeric operator is applied
/// * the result is wrapped in a Nullable{UnderlyingResultType}.
diff --git a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs
index 4adcaab8d..6910e8300 100644
--- a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs
+++ b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs
@@ -85,7 +85,35 @@ namespace ICSharpCode.Decompiler.IL
}
}
- partial class Comp
+ public enum ComparisonLiftingKind
+ {
+ ///
+ /// Not a lifted comparison.
+ ///
+ None,
+ ///
+ /// C#-style lifted comparison:
+ /// * operands that have a ResultType != this.InputType are expected to return a value of
+ /// type Nullable{T}, where T.GetStackType() == this.InputType.
+ /// * if both operands are null, equality comparisons evaluate to 1, all other comparisons to 0.
+ /// * if one operand is null, inequality comparisons evaluate to 1, all other comparisons to 0.
+ /// * if neither operand is null, the underlying comparison is performed.
+ ///
+ /// Note that even though C#-style lifted comparisons set IsLifted=true,
+ /// the ResultType remains I4 as with normal comparisons.
+ ///
+ CSharp,
+ ///
+ /// SQL-style lifted comparison: works like a lifted binary numeric instruction,
+ /// that is, if any input operand is null, the comparison evaluates to null.
+ ///
+ ///
+ /// This lifting kind is currently not used.
+ ///
+ ThreeValuedLogic
+ }
+
+ partial class Comp : ILiftableInstruction
{
ComparisonKind kind;
@@ -97,12 +125,13 @@ namespace ICSharpCode.Decompiler.IL
}
}
+ public readonly ComparisonLiftingKind LiftingKind;
+
///
/// Gets the stack type of the comparison inputs.
+ /// For lifted comparisons, this is the underlying input type.
///
- public StackType InputType {
- get { return Left.ResultType; }
- }
+ public StackType InputType;
///
/// If this is an integer comparison, specifies the sign used to interpret the integers.
@@ -112,10 +141,36 @@ namespace ICSharpCode.Decompiler.IL
public Comp(ComparisonKind kind, Sign sign, ILInstruction left, ILInstruction right) : base(OpCode.Comp, left, right)
{
this.kind = kind;
+ this.LiftingKind = ComparisonLiftingKind.None;
+ this.InputType = left.ResultType;
this.Sign = sign;
Debug.Assert(left.ResultType == right.ResultType);
}
+ public Comp(ComparisonKind kind, ComparisonLiftingKind lifting, StackType inputType, Sign sign, ILInstruction left, ILInstruction right) : base(OpCode.Comp, left, right)
+ {
+ this.kind = kind;
+ this.LiftingKind = lifting;
+ this.InputType = inputType;
+ this.Sign = sign;
+ }
+
+ public override StackType ResultType => LiftingKind == ComparisonLiftingKind.ThreeValuedLogic ? StackType.O : StackType.I4;
+ public bool IsLifted => LiftingKind != ComparisonLiftingKind.None;
+ public StackType UnderlyingResultType => StackType.I4;
+
+ internal override void CheckInvariant(ILPhase phase)
+ {
+ base.CheckInvariant(phase);
+ if (LiftingKind == ComparisonLiftingKind.None) {
+ Debug.Assert(Left.ResultType == InputType);
+ Debug.Assert(Right.ResultType == InputType);
+ } else {
+ Debug.Assert(Left.ResultType == InputType || Left.ResultType == StackType.O);
+ Debug.Assert(Right.ResultType == InputType || Right.ResultType == StackType.O);
+ }
+ }
+
public override void WriteTo(ITextOutput output)
{
output.Write(OpCode);
@@ -127,6 +182,14 @@ namespace ICSharpCode.Decompiler.IL
output.Write(".unsigned");
break;
}
+ switch (LiftingKind) {
+ case ComparisonLiftingKind.CSharp:
+ output.Write(".lifted[C#]");
+ break;
+ case ComparisonLiftingKind.ThreeValuedLogic:
+ output.Write(".lifted[3VL]");
+ break;
+ }
output.Write('(');
Left.WriteTo(output);
output.Write(' ');
diff --git a/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs
index 15bc3ac61..49b5166b7 100644
--- a/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs
+++ b/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs
@@ -707,7 +707,19 @@ namespace ICSharpCode.Decompiler.IL
public interface ILiftableInstruction
{
+ ///
+ /// Gets whether the instruction was lifted; that is, whether is accepts
+ /// potentially nullable arguments.
+ ///
bool IsLifted { get; }
+
+ ///
+ /// If the instruction is lifted and returns a nullable result,
+ /// gets the underlying result type.
+ ///
+ /// Note that not all lifted instructions return a nullable result:
+ /// C# comparisons always return a bool!
+ ///
StackType UnderlyingResultType { get; }
}
}
diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
index 942e387f9..209311435 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
@@ -55,6 +55,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms
protected internal override void VisitComp(Comp inst)
{
base.VisitComp(inst);
+ if (inst.IsLifted) {
+ return;
+ }
if (inst.Right.MatchLdNull()) {
if (inst.Kind == ComparisonKind.GreaterThan) {
context.Step("comp(left > ldnull) => comp(left != ldnull)", inst);
@@ -84,6 +87,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
// => comp(ldlen.i4 array > ldc.i4 0)
// This is a special case where the C# compiler doesn't generate conv.i4 after ldlen.
context.Step("comp(ldlen.i4 array > ldc.i4 0)", inst);
+ inst.InputType = StackType.I4;
inst.Left.ReplaceWith(new LdLen(StackType.I4, array) { ILRange = inst.Left.ILRange });
inst.Right = rightWithoutConv;
}
@@ -143,9 +147,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
arg.AddILRange(inst.Argument.ILRange);
inst.ReplaceWith(arg);
arg.AcceptVisitor(this);
- } else if (inst.Argument is Comp) {
- Comp comp = (Comp)inst.Argument;
- if (comp.InputType != StackType.F || comp.Kind.IsEqualityOrInequality()) {
+ } else if (inst.Argument is Comp comp) {
+ if ((comp.InputType != StackType.F && !comp.IsLifted) || comp.Kind.IsEqualityOrInequality()) {
context.Step("push negation into comparison", inst);
comp.Kind = comp.Kind.Negate();
comp.AddILRange(inst.ILRange);
diff --git a/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs
index 0ff7cf2d6..6af06fda8 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs
@@ -55,16 +55,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{
if (!context.Settings.LiftNullables)
return false;
- // Detect pattern:
- // if (condition)
- // newobj Nullable..ctor(exprToLift)
- // else
- // default.value System.Nullable
- if (!AnalyzeTopLevelCondition(ifInst.Condition, out bool negativeCondition))
- return false;
- ILInstruction trueInst = negativeCondition ? ifInst.FalseInst : ifInst.TrueInst;
- ILInstruction falseInst = negativeCondition ? ifInst.TrueInst : ifInst.FalseInst;
- var lifted = Lift(trueInst, falseInst, ifInst.ILRange);
+ var lifted = Lift(ifInst, ifInst.TrueInst, ifInst.FalseInst);
if (lifted != null) {
ifInst.ReplaceWith(lifted);
return true;
@@ -76,6 +67,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{
if (!context.Settings.LiftNullables)
return false;
+ /// e.g.:
// if (!condition) Block {
// leave IL_0000 (default.value System.Nullable`1[[System.Int64]])
// }
@@ -92,11 +84,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false;
if (elseLeave.TargetContainer != thenLeave.TargetContainer)
return false;
- if (!AnalyzeTopLevelCondition(ifInst.Condition, out bool negativeCondition))
- return false;
- ILInstruction trueInst = negativeCondition ? elseLeave.Value : thenLeave.Value;
- ILInstruction falseInst = negativeCondition ? thenLeave.Value : elseLeave.Value;
- var lifted = Lift(trueInst, falseInst, ifInst.ILRange);
+ var lifted = Lift(ifInst, thenLeave.Value, elseLeave.Value);
if (lifted != null) {
thenLeave.Value = lifted;
ifInst.ReplaceWith(thenLeave);
@@ -108,16 +96,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms
#endregion
#region AnalyzeCondition
- bool AnalyzeTopLevelCondition(ILInstruction condition, out bool negativeCondition)
- {
- negativeCondition = false;
- while (condition.MatchLogicNot(out var arg)) {
- condition = arg;
- negativeCondition = !negativeCondition;
- }
- return AnalyzeCondition(condition);
- }
-
bool AnalyzeCondition(ILInstruction condition)
{
if (MatchHasValueCall(condition, out var v)) {
@@ -134,7 +112,79 @@ namespace ICSharpCode.Decompiler.IL.Transforms
}
#endregion
- #region DoLift
+ #region Lift / DoLift
+ ILInstruction Lift(IfInstruction ifInst, ILInstruction trueInst, ILInstruction falseInst)
+ {
+ ILInstruction condition = ifInst.Condition;
+ while (condition.MatchLogicNot(out var arg)) {
+ condition = arg;
+ Swap(ref trueInst, ref falseInst);
+ }
+ if (AnalyzeCondition(condition)) {
+ // (v1 != null && ... && vn != null) ? trueInst : falseInst
+ // => normal lifting
+ return LiftNormal(trueInst, falseInst, ilrange: ifInst.ILRange);
+ }
+ if (condition is Comp comp && !comp.IsLifted && !comp.Kind.IsEqualityOrInequality()) {
+ // This might be a C#-style lifted comparison
+ // (C# checks the underlying value before checking the HasValue bits)
+ if (falseInst.MatchLdcI4(0) && AnalyzeCondition(trueInst)) {
+ // comp(lhs, rhs) ? (v1 != null && ... && vn != null) : false
+ // => comp.lifted[C#](lhs, rhs)
+ return LiftCSharpComparison(comp, comp.Kind);
+ } else if (trueInst.MatchLdcI4(0) && AnalyzeCondition(falseInst)) {
+ // comp(lhs, rhs) ? false : (v1 != null && ... && vn != null)
+ return LiftCSharpComparison(comp, comp.Kind.Negate());
+ }
+ }
+ return null;
+
+ }
+
+ static void Swap(ref T a, ref T b)
+ {
+ T tmp = a;
+ a = b;
+ b = tmp;
+ }
+
+ ///
+ /// Lift a C# comparison.
+ ///
+ /// The output instructions should evaluate to false when any of the nullableVars is null.
+ /// Otherwise, the output instruction should evaluate to the same value as the input instruction.
+ /// The output instruction should have the same side-effects (incl. exceptions being thrown) as the input instruction.
+ /// This means unlike LiftNormal(), we cannot rely on the input instruction not being evaluated if
+ /// a variable is null.
+ ///
+ Comp LiftCSharpComparison(Comp comp, ComparisonKind newComparisonKind)
+ {
+ var (left, leftBits) = DoLift(comp.Left);
+ var (right, rightBits) = DoLift(comp.Right);
+ if (left != null && right == null && SemanticHelper.IsPure(comp.Right.Flags)) {
+ // Embed non-nullable pure expression in lifted expression.
+ right = comp.Right.Clone();
+ }
+ if (left == null && right != null && SemanticHelper.IsPure(comp.Left.Flags)) {
+ // Embed non-nullable pure expression in lifted expression.
+ left = comp.Left.Clone();
+ }
+ // due to the restrictions on side effects, we only allow instructions that are pure after lifting.
+ // (we can't check this before lifting due to the calls to GetValueOrDefault())
+ if (left != null && right != null && SemanticHelper.IsPure(left.Flags) && SemanticHelper.IsPure(right.Flags)) {
+ var bits = leftBits ?? rightBits;
+ if (rightBits != null)
+ bits.UnionWith(rightBits);
+ if (!bits.All(0, nullableVars.Count)) {
+ // don't lift if a nullableVar doesn't contribute to the result
+ return null;
+ }
+ context.Step("NullableLiftingTransform: C# comparison", comp);
+ return new Comp(newComparisonKind, ComparisonLiftingKind.CSharp, comp.InputType, comp.Sign, left, right);
+ }
+ return null;
+ }
+
///
/// Performs nullable lifting.
///
@@ -143,7 +193,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// where the v1,...,vn are the this.nullableVars.
/// If lifting fails, returns null.
///
- ILInstruction Lift(ILInstruction trueInst, ILInstruction falseInst, Interval ilrange)
+ ILInstruction LiftNormal(ILInstruction trueInst, ILInstruction falseInst, Interval ilrange)
{
bool isNullCoalescingWithNonNullableFallback = false;
if (!MatchNullableCtor(trueInst, out var utype, out var exprToLift)) {