// Copyright (c) 2017 Daniel Grunwald // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Diagnostics; using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.Decompiler.IL.Transforms { /// /// Nullable lifting gets run in two places: /// * the usual form looks at an if-else, and runs within the ExpressionTransforms /// * the NullableLiftingBlockTransform handles the cases where Roslyn generates /// two 'ret' statements for the null/non-null cases of a lifted operator. /// struct NullableLiftingTransform { readonly ILTransformContext context; List nullableVars; public NullableLiftingTransform(ILTransformContext context) { this.context = context; this.nullableVars = null; } #region Run /// /// Main entry point into the normal code path of this transform. /// Called by expression transform. /// public bool Run(IfInstruction ifInst) { 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(ifInst, trueInst, falseInst); if (lifted != null) { ifInst.ReplaceWith(lifted); return true; } return false; } public bool RunBlock(Block block) { if (!context.Settings.LiftNullables) return false; // if (!condition) Block { // leave IL_0000 (default.value System.Nullable`1[[System.Int64]]) // } // leave IL_0000 (newobj .ctor(exprToLift)) IfInstruction ifInst; if (block.Instructions.Last() is Leave elseLeave) { ifInst = block.Instructions.SecondToLastOrDefault() as IfInstruction; if (ifInst == null || !ifInst.FalseInst.MatchNop()) return false; } else { return false; } if (!(Block.Unwrap(ifInst.TrueInst) is Leave thenLeave)) 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(ifInst, trueInst, falseInst); if (lifted != null) { thenLeave.Value = lifted; ifInst.ReplaceWith(thenLeave); block.Instructions.Remove(elseLeave); return true; } return false; } #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)) { if (nullableVars == null) nullableVars = new List(); nullableVars.Add(v); return true; } else if (condition is BinaryNumericInstruction bitand) { if (!(bitand.Operator == BinaryNumericOperator.BitAnd && bitand.ResultType == StackType.I4)) return false; return AnalyzeCondition(bitand.Left) && AnalyzeCondition(bitand.Right); } return false; } #endregion #region DoLift ILInstruction Lift(IfInstruction ifInst, ILInstruction trueInst, ILInstruction falseInst) { if (!MatchNullableCtor(trueInst, out var utype, out var exprToLift)) return null; if (!MatchNull(falseInst, utype)) return null; ILInstruction lifted; if (nullableVars.Count == 1 && MatchGetValueOrDefault(exprToLift, nullableVars[0])) { // v != null ? call GetValueOrDefault(ldloca v) : null // => conv.nop.lifted(ldloc v) // This case is handled separately from DoLift() because // that doesn't introduce nop-conversions. context.Step("if => conv.nop.lifted", ifInst); var inputUType = NullableType.GetUnderlyingType(nullableVars[0].Type); lifted = new Conv( new LdLoc(nullableVars[0]), inputUType.GetStackType(), inputUType.GetSign(), utype.ToPrimitiveType(), checkForOverflow: false, isLifted: true ) { ILRange = ifInst.ILRange }; } else { context.Step("NullableLiftingTransform.DoLift", ifInst); lifted = DoLift(exprToLift); } if (lifted != null) { Debug.Assert(lifted is ILiftableInstruction liftable && liftable.IsLifted && liftable.UnderlyingResultType == exprToLift.ResultType); } return lifted; } // Lifts the specified instruction. // Creates a new lifted instruction without modifying the input instruction. // If lifting fails, returns null. ILInstruction DoLift(ILInstruction inst) { if (MatchGetValueOrDefault(inst, out ILVariable inputVar) && nullableVars.Contains(inputVar)) { // n.GetValueOrDefault() lifted => n. return new LdLoc(inputVar) { ILRange = inst.ILRange }; } else if (inst is Conv conv) { var arg = DoLift(conv.Argument); if (arg != null) { return new Conv(arg, conv.InputType, conv.InputSign, conv.TargetType, conv.CheckForOverflow, isLifted: true) { ILRange = conv.ILRange }; } } else if (inst is BinaryNumericInstruction binary) { var left = DoLift(binary.Left); var right = DoLift(binary.Right); if (left != null && right == null && SemanticHelper.IsPure(binary.Right.Flags)) { // Embed non-nullable pure expression in lifted expression. right = binary.Right.Clone(); } if (left == null && right != null && SemanticHelper.IsPure(binary.Left.Flags)) { // Embed non-nullable pure expression in lifted expression. left = binary.Left.Clone(); } if (left != null && right != null) { return new BinaryNumericInstruction( binary.Operator, left, right, binary.LeftInputType, binary.RightInputType, binary.CheckForOverflow, binary.Sign, isLifted: true ) { ILRange = binary.ILRange }; } } return null; } #endregion #region Match...Call /// /// Matches 'call get_HasValue(ldloca v)' /// static bool MatchHasValueCall(ILInstruction inst, out ILVariable v) { v = null; if (!(inst is Call call)) return false; if (call.Arguments.Count != 1) return false; if (call.Method.Name != "get_HasValue") return false; if (call.Method.DeclaringTypeDefinition?.KnownTypeCode != KnownTypeCode.NullableOfT) return false; return call.Arguments[0].MatchLdLoca(out v); } /// /// Matches 'newobj Nullable{underlyingType}.ctor(arg)' /// static bool MatchNullableCtor(ILInstruction inst, out IType underlyingType, out ILInstruction arg) { underlyingType = null; arg = null; if (!(inst is NewObj newobj)) return false; if (!newobj.Method.IsConstructor || newobj.Arguments.Count != 1) return false; if (newobj.Method.DeclaringTypeDefinition?.KnownTypeCode != KnownTypeCode.NullableOfT) return false; arg = newobj.Arguments[0]; underlyingType = NullableType.GetUnderlyingType(newobj.Method.DeclaringType); return true; } /// /// Matches 'call Nullable{T}.GetValueOrDefault(arg)' /// static bool MatchGetValueOrDefault(ILInstruction inst, out ILInstruction arg) { arg = null; if (!(inst is Call call)) return false; if (call.Method.Name != "GetValueOrDefault" || call.Arguments.Count != 1) return false; if (call.Method.DeclaringTypeDefinition?.KnownTypeCode != KnownTypeCode.NullableOfT) return false; arg = call.Arguments[0]; return true; } /// /// Matches 'call Nullable{T}.GetValueOrDefault(ldloca v)' /// static bool MatchGetValueOrDefault(ILInstruction inst, out ILVariable v) { v = null; return MatchGetValueOrDefault(inst, out ILInstruction arg) && arg.MatchLdLoca(out v); } /// /// Matches 'call Nullable{T}.GetValueOrDefault(ldloca v)' /// static bool MatchGetValueOrDefault(ILInstruction inst, ILVariable v) { return MatchGetValueOrDefault(inst, out ILVariable v2) && v == v2; } static bool MatchNull(ILInstruction inst, out IType underlyingType) { underlyingType = null; if (inst.MatchDefaultValue(out IType type)) { underlyingType = NullableType.GetUnderlyingType(type); return NullableType.IsNullable(type); } underlyingType = null; return false; } static bool MatchNull(ILInstruction inst, IType underlyingType) { return MatchNull(inst, out var utype) && utype.Equals(underlyingType); } #endregion } class NullableLiftingBlockTransform : IBlockTransform { public void Run(Block block, BlockTransformContext context) { new NullableLiftingTransform(context).RunBlock(block); } } }