Browse Source

[nullables] Lifting for non-equality comparisons.

pull/870/head
Daniel Grunwald 8 years ago
parent
commit
21ef967d4d
  1. 15
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs
  2. 11
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  3. 8
      ICSharpCode.Decompiler/IL/Instructions.cs
  4. 8
      ICSharpCode.Decompiler/IL/Instructions.tt
  5. 2
      ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs
  6. 71
      ICSharpCode.Decompiler/IL/Instructions/Comp.cs
  7. 12
      ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs
  8. 9
      ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
  9. 104
      ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs

15
ICSharpCode.Decompiler.Tests/TestCases/Pretty/LiftedOperators.cs

@ -648,6 +648,21 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -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

11
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -423,6 +423,9 @@ namespace ICSharpCode.Decompiler.CSharp @@ -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 @@ -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)

8
ICSharpCode.Decompiler/IL/Instructions.cs

@ -881,7 +881,7 @@ namespace ICSharpCode.Decompiler.IL @@ -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 @@ -1757,7 +1757,7 @@ namespace ICSharpCode.Decompiler.IL
/// <summary>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).</summary>
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 @@ -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 @@ -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;
}
}
}

8
ICSharpCode.Decompiler/IL/Instructions.tt

@ -67,7 +67,7 @@ @@ -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 @@ @@ -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 @@ @@ -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)",

2
ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs

@ -65,7 +65,7 @@ namespace ICSharpCode.Decompiler.IL @@ -65,7 +65,7 @@ namespace ICSharpCode.Decompiler.IL
/// <remarks>
/// 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}.

71
ICSharpCode.Decompiler/IL/Instructions/Comp.cs

@ -85,7 +85,35 @@ namespace ICSharpCode.Decompiler.IL @@ -85,7 +85,35 @@ namespace ICSharpCode.Decompiler.IL
}
}
partial class Comp
public enum ComparisonLiftingKind
{
/// <summary>
/// Not a lifted comparison.
/// </summary>
None,
/// <summary>
/// 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 <c>null</c>, equality comparisons evaluate to 1, all other comparisons to 0.
/// * if one operand is <c>null</c>, inequality comparisons evaluate to 1, all other comparisons to 0.
/// * if neither operand is <c>null</c>, the underlying comparison is performed.
///
/// Note that even though C#-style lifted comparisons set IsLifted=true,
/// the ResultType remains I4 as with normal comparisons.
/// </summary>
CSharp,
/// <summary>
/// SQL-style lifted comparison: works like a lifted binary numeric instruction,
/// that is, if any input operand is <c>null</c>, the comparison evaluates to <c>null</c>.
/// </summary>
/// <remarks>
/// This lifting kind is currently not used.
/// </remarks>
ThreeValuedLogic
}
partial class Comp : ILiftableInstruction
{
ComparisonKind kind;
@ -97,12 +125,13 @@ namespace ICSharpCode.Decompiler.IL @@ -97,12 +125,13 @@ namespace ICSharpCode.Decompiler.IL
}
}
public readonly ComparisonLiftingKind LiftingKind;
/// <summary>
/// Gets the stack type of the comparison inputs.
/// For lifted comparisons, this is the underlying input type.
/// </summary>
public StackType InputType {
get { return Left.ResultType; }
}
public StackType InputType;
/// <summary>
/// If this is an integer comparison, specifies the sign used to interpret the integers.
@ -112,10 +141,36 @@ namespace ICSharpCode.Decompiler.IL @@ -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 @@ -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(' ');

12
ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs

@ -707,7 +707,19 @@ namespace ICSharpCode.Decompiler.IL @@ -707,7 +707,19 @@ namespace ICSharpCode.Decompiler.IL
public interface ILiftableInstruction
{
/// <summary>
/// Gets whether the instruction was lifted; that is, whether is accepts
/// potentially nullable arguments.
/// </summary>
bool IsLifted { get; }
/// <summary>
/// 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!
/// </summary>
StackType UnderlyingResultType { get; }
}
}

9
ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs

@ -55,6 +55,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -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 @@ -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 @@ -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);

104
ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs

@ -55,16 +55,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -55,16 +55,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{
if (!context.Settings.LiftNullables)
return false;
// Detect pattern:
// if (condition)
// newobj Nullable<utype>..ctor(exprToLift)
// else
// default.value System.Nullable<utype>
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 @@ -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 @@ -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 @@ -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 @@ -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<T>(ref T a, ref T b)
{
T tmp = a;
a = b;
b = tmp;
}
/// <summary>
/// Lift a C# comparison.
///
/// The output instructions should evaluate to <c>false</c> when any of the <c>nullableVars</c> is <c>null</c>.
/// 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 <c>null</c>.
/// </summary>
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;
}
/// <summary>
/// Performs nullable lifting.
///
@ -143,7 +193,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -143,7 +193,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// where the v1,...,vn are the <c>this.nullableVars</c>.
/// If lifting fails, returns <c>null</c>.
/// </summary>
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)) {

Loading…
Cancel
Save