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)) {