You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

870 lines
34 KiB

  1. // Copyright (c) 2017 Daniel Grunwald
  2. //
  3. // Permission is hereby granted, free of charge, to any person obtaining a copy of this
  4. // software and associated documentation files (the "Software"), to deal in the Software
  5. // without restriction, including without limitation the rights to use, copy, modify, merge,
  6. // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
  7. // to whom the Software is furnished to do so, subject to the following conditions:
  8. //
  9. // The above copyright notice and this permission notice shall be included in all copies or
  10. // substantial portions of the Software.
  11. //
  12. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  13. // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
  14. // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
  15. // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  16. // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  17. // DEALINGS IN THE SOFTWARE.
  18. using System;
  19. using System.Collections.Generic;
  20. using System.Diagnostics;
  21. using System.Linq;
  22. using ICSharpCode.Decompiler.TypeSystem;
  23. using ICSharpCode.Decompiler.Util;
  24. namespace ICSharpCode.Decompiler.IL.Transforms
  25. {
  26. /// <summary>
  27. /// Nullable lifting gets run in two places:
  28. /// * the usual form looks at an if-else, and runs within the ExpressionTransforms.
  29. /// * the NullableLiftingBlockTransform handles the cases where Roslyn generates
  30. /// two 'ret' statements for the null/non-null cases of a lifted operator.
  31. ///
  32. /// The transform handles the following languages constructs:
  33. /// * lifted conversions
  34. /// * lifted unary and binary operators
  35. /// * lifted comparisons
  36. /// * the ?? operator with type Nullable{T} on the left-hand-side
  37. /// </summary>
  38. struct NullableLiftingTransform
  39. {
  40. readonly ILTransformContext context;
  41. List<ILVariable> nullableVars;
  42. public NullableLiftingTransform(ILTransformContext context)
  43. {
  44. this.context = context;
  45. this.nullableVars = null;
  46. }
  47. #region Run
  48. /// <summary>
  49. /// Main entry point into the normal code path of this transform.
  50. /// Called by expression transform.
  51. /// </summary>
  52. public bool Run(IfInstruction ifInst)
  53. {
  54. if (!context.Settings.LiftNullables)
  55. return false;
  56. var lifted = Lift(ifInst, ifInst.TrueInst, ifInst.FalseInst);
  57. if (lifted != null) {
  58. ifInst.ReplaceWith(lifted);
  59. return true;
  60. }
  61. return false;
  62. }
  63. public bool RunStatements(Block block, int pos)
  64. {
  65. if (!context.Settings.LiftNullables)
  66. return false;
  67. /// e.g.:
  68. // if (!condition) Block {
  69. // leave IL_0000 (default.value System.Nullable`1[[System.Int64]])
  70. // }
  71. // leave IL_0000 (newobj .ctor(exprToLift))
  72. if (pos != block.Instructions.Count - 2)
  73. return false;
  74. if (!(block.Instructions[pos] is IfInstruction ifInst))
  75. return false;
  76. if (!(Block.Unwrap(ifInst.TrueInst) is Leave thenLeave))
  77. return false;
  78. if (!ifInst.FalseInst.MatchNop())
  79. return false;
  80. if (!(block.Instructions[pos + 1] is Leave elseLeave))
  81. return false;
  82. if (elseLeave.TargetContainer != thenLeave.TargetContainer)
  83. return false;
  84. var lifted = Lift(ifInst, thenLeave.Value, elseLeave.Value);
  85. if (lifted != null) {
  86. thenLeave.Value = lifted;
  87. ifInst.ReplaceWith(thenLeave);
  88. block.Instructions.Remove(elseLeave);
  89. return true;
  90. }
  91. return false;
  92. }
  93. #endregion
  94. #region AnalyzeCondition
  95. bool AnalyzeCondition(ILInstruction condition)
  96. {
  97. if (MatchHasValueCall(condition, out var v)) {
  98. if (nullableVars == null)
  99. nullableVars = new List<ILVariable>();
  100. nullableVars.Add(v);
  101. return true;
  102. } else if (condition is BinaryNumericInstruction bitand) {
  103. if (!(bitand.Operator == BinaryNumericOperator.BitAnd && bitand.ResultType == StackType.I4))
  104. return false;
  105. return AnalyzeCondition(bitand.Left) && AnalyzeCondition(bitand.Right);
  106. }
  107. return false;
  108. }
  109. #endregion
  110. #region Main lifting logic
  111. /// <summary>
  112. /// Main entry point for lifting; called by both the expression-transform
  113. /// and the block transform.
  114. /// </summary>
  115. ILInstruction Lift(IfInstruction ifInst, ILInstruction trueInst, ILInstruction falseInst)
  116. {
  117. ILInstruction condition = ifInst.Condition;
  118. while (condition.MatchLogicNot(out var arg)) {
  119. condition = arg;
  120. Swap(ref trueInst, ref falseInst);
  121. }
  122. if (AnalyzeCondition(condition)) {
  123. // (v1 != null && ... && vn != null) ? trueInst : falseInst
  124. // => normal lifting
  125. return LiftNormal(trueInst, falseInst, ilrange: ifInst.ILRange);
  126. }
  127. if (MatchCompOrDecimal(condition, out var comp)) {
  128. // This might be a C#-style lifted comparison
  129. // (C# checks the underlying value before checking the HasValue bits)
  130. if (comp.Kind.IsEqualityOrInequality()) {
  131. // for equality/inequality, the HasValue bits must also compare equal/inequal
  132. if (comp.Kind == ComparisonKind.Inequality) {
  133. // handle inequality by swapping one last time
  134. Swap(ref trueInst, ref falseInst);
  135. }
  136. if (falseInst.MatchLdcI4(0)) {
  137. // (a.GetValueOrDefault() == b.GetValueOrDefault()) ? (a.HasValue == b.HasValue) : false
  138. // => a == b
  139. return LiftCSharpEqualityComparison(comp, ComparisonKind.Equality, trueInst)
  140. ?? LiftCSharpUserEqualityComparison(comp, ComparisonKind.Equality, trueInst);
  141. } else if (falseInst.MatchLdcI4(1)) {
  142. // (a.GetValueOrDefault() == b.GetValueOrDefault()) ? (a.HasValue != b.HasValue) : true
  143. // => a != b
  144. return LiftCSharpEqualityComparison(comp, ComparisonKind.Inequality, trueInst)
  145. ?? LiftCSharpUserEqualityComparison(comp, ComparisonKind.Inequality, trueInst);
  146. } else if (IsGenericNewPattern(comp.Left, comp.Right, trueInst, falseInst)) {
  147. // (default(T) == null) ? Activator.CreateInstance<T>() : default(T)
  148. // => Activator.CreateInstance<T>()
  149. return trueInst;
  150. }
  151. } else {
  152. // Not (in)equality, but one of < <= > >=.
  153. // Returns false unless all HasValue bits are true.
  154. if (falseInst.MatchLdcI4(0) && AnalyzeCondition(trueInst)) {
  155. // comp(lhs, rhs) ? (v1 != null && ... && vn != null) : false
  156. // => comp.lifted[C#](lhs, rhs)
  157. return LiftCSharpComparison(comp, comp.Kind);
  158. } else if (trueInst.MatchLdcI4(0) && AnalyzeCondition(falseInst)) {
  159. // comp(lhs, rhs) ? false : (v1 != null && ... && vn != null)
  160. return LiftCSharpComparison(comp, comp.Kind.Negate());
  161. }
  162. }
  163. }
  164. ILVariable v;
  165. // Handle equality comparisons with bool?:
  166. if (MatchGetValueOrDefault(condition, out v)
  167. && NullableType.GetUnderlyingType(v.Type).IsKnownType(KnownTypeCode.Boolean))
  168. {
  169. if (MatchHasValueCall(trueInst, v) && falseInst.MatchLdcI4(0)) {
  170. // v.GetValueOrDefault() ? v.HasValue : false
  171. // ==> v == true
  172. context.Step("NullableLiftingTransform: v == true", ifInst);
  173. return new Comp(ComparisonKind.Equality, ComparisonLiftingKind.CSharp,
  174. StackType.I4, Sign.None,
  175. new LdLoc(v) { ILRange = trueInst.ILRange },
  176. new LdcI4(1) { ILRange = falseInst.ILRange }
  177. ) { ILRange = ifInst.ILRange };
  178. } else if (trueInst.MatchLdcI4(0) && MatchHasValueCall(falseInst, v)) {
  179. // v.GetValueOrDefault() ? false : v.HasValue
  180. // ==> v == false
  181. context.Step("NullableLiftingTransform: v == false", ifInst);
  182. return new Comp(ComparisonKind.Equality, ComparisonLiftingKind.CSharp,
  183. StackType.I4, Sign.None,
  184. new LdLoc(v) { ILRange = falseInst.ILRange },
  185. trueInst // LdcI4(0)
  186. ) { ILRange = ifInst.ILRange };
  187. } else if (MatchNegatedHasValueCall(trueInst, v) && falseInst.MatchLdcI4(1)) {
  188. // v.GetValueOrDefault() ? !v.HasValue : true
  189. // ==> v != true
  190. context.Step("NullableLiftingTransform: v != true", ifInst);
  191. return new Comp(ComparisonKind.Inequality, ComparisonLiftingKind.CSharp,
  192. StackType.I4, Sign.None,
  193. new LdLoc(v) { ILRange = trueInst.ILRange },
  194. falseInst // LdcI4(1)
  195. ) { ILRange = ifInst.ILRange };
  196. } else if (trueInst.MatchLdcI4(1) && MatchNegatedHasValueCall(falseInst, v)) {
  197. // v.GetValueOrDefault() ? true : !v.HasValue
  198. // ==> v != false
  199. context.Step("NullableLiftingTransform: v != false", ifInst);
  200. return new Comp(ComparisonKind.Inequality, ComparisonLiftingKind.CSharp,
  201. StackType.I4, Sign.None,
  202. new LdLoc(v) { ILRange = falseInst.ILRange },
  203. new LdcI4(0) { ILRange = trueInst.ILRange }
  204. ) { ILRange = ifInst.ILRange };
  205. }
  206. }
  207. // Handle & and | on bool?:
  208. if (trueInst.MatchLdLoc(out v)) {
  209. if (MatchNullableCtor(falseInst, out var utype, out var arg)
  210. && utype.IsKnownType(KnownTypeCode.Boolean) && arg.MatchLdcI4(0))
  211. {
  212. // condition ? v : (bool?)false
  213. // => condition & v
  214. context.Step("NullableLiftingTransform: 3vl.logic.and(bool, bool?)", ifInst);
  215. return new ThreeValuedLogicAnd(condition, trueInst) { ILRange = ifInst.ILRange };
  216. }
  217. if (falseInst.MatchLdLoc(out var v2)) {
  218. // condition ? v : v2
  219. if (MatchThreeValuedLogicConditionPattern(condition, out var nullable1, out var nullable2)) {
  220. // (nullable1.GetValueOrDefault() || (!nullable2.GetValueOrDefault() && !nullable1.HasValue)) ? v : v2
  221. if (v == nullable1 && v2 == nullable2) {
  222. context.Step("NullableLiftingTransform: 3vl.logic.or(bool?, bool?)", ifInst);
  223. return new ThreeValuedLogicOr(trueInst, falseInst) { ILRange = ifInst.ILRange };
  224. } else if (v == nullable2 && v2 == nullable1) {
  225. context.Step("NullableLiftingTransform: 3vl.logic.and(bool?, bool?)", ifInst);
  226. return new ThreeValuedLogicAnd(falseInst, trueInst) { ILRange = ifInst.ILRange };
  227. }
  228. }
  229. }
  230. } else if (falseInst.MatchLdLoc(out v)) {
  231. if (MatchNullableCtor(trueInst, out var utype, out var arg)
  232. && utype.IsKnownType(KnownTypeCode.Boolean) && arg.MatchLdcI4(1)) {
  233. // condition ? (bool?)true : v
  234. // => condition | v
  235. context.Step("NullableLiftingTransform: 3vl.logic.or(bool, bool?)", ifInst);
  236. return new ThreeValuedLogicOr(condition, falseInst) { ILRange = ifInst.ILRange };
  237. }
  238. }
  239. return null;
  240. }
  241. private bool IsGenericNewPattern(ILInstruction compLeft, ILInstruction compRight, ILInstruction trueInst, ILInstruction falseInst)
  242. {
  243. // (default(T) == null) ? Activator.CreateInstance<T>() : default(T)
  244. return falseInst.MatchDefaultValue(out var type) &&
  245. (trueInst is Call c && c.Method.FullName == "System.Activator.CreateInstance" && c.Method.TypeArguments.Count == 1) &&
  246. type.Kind == TypeKind.TypeParameter &&
  247. compLeft.MatchDefaultValue(out var type2) &&
  248. type.Equals(type2) &&
  249. compRight.MatchLdNull();
  250. }
  251. private bool MatchThreeValuedLogicConditionPattern(ILInstruction condition, out ILVariable nullable1, out ILVariable nullable2)
  252. {
  253. // Try to match: nullable1.GetValueOrDefault() || (!nullable2.GetValueOrDefault() && !nullable1.HasValue)
  254. nullable1 = null;
  255. nullable2 = null;
  256. if (!condition.MatchLogicOr(out var lhs, out var rhs))
  257. return false;
  258. if (!MatchGetValueOrDefault(lhs, out nullable1))
  259. return false;
  260. if (!NullableType.GetUnderlyingType(nullable1.Type).IsKnownType(KnownTypeCode.Boolean))
  261. return false;
  262. if (!rhs.MatchLogicAnd(out lhs, out rhs))
  263. return false;
  264. if (!lhs.MatchLogicNot(out var arg))
  265. return false;
  266. if (!MatchGetValueOrDefault(arg, out nullable2))
  267. return false;
  268. if (!NullableType.GetUnderlyingType(nullable2.Type).IsKnownType(KnownTypeCode.Boolean))
  269. return false;
  270. if (!rhs.MatchLogicNot(out arg))
  271. return false;
  272. return MatchHasValueCall(arg, nullable1);
  273. }
  274. static void Swap<T>(ref T a, ref T b)
  275. {
  276. T tmp = a;
  277. a = b;
  278. b = tmp;
  279. }
  280. #endregion
  281. #region CSharpComp
  282. static bool MatchCompOrDecimal(ILInstruction inst, out CompOrDecimal result)
  283. {
  284. result = default(CompOrDecimal);
  285. result.Instruction = inst;
  286. if (inst is Comp comp && !comp.IsLifted) {
  287. result.Kind = comp.Kind;
  288. result.Left = comp.Left;
  289. result.Right = comp.Right;
  290. return true;
  291. } else if (inst is Call call && call.Method.IsOperator && call.Arguments.Count == 2 && !call.IsLifted) {
  292. switch (call.Method.Name) {
  293. case "op_Equality":
  294. result.Kind = ComparisonKind.Equality;
  295. break;
  296. case "op_Inequality":
  297. result.Kind = ComparisonKind.Inequality;
  298. break;
  299. case "op_LessThan":
  300. result.Kind = ComparisonKind.LessThan;
  301. break;
  302. case "op_LessThanOrEqual":
  303. result.Kind = ComparisonKind.LessThanOrEqual;
  304. break;
  305. case "op_GreaterThan":
  306. result.Kind = ComparisonKind.GreaterThan;
  307. break;
  308. case "op_GreaterThanOrEqual":
  309. result.Kind = ComparisonKind.GreaterThanOrEqual;
  310. break;
  311. default:
  312. return false;
  313. }
  314. result.Left = call.Arguments[0];
  315. result.Right = call.Arguments[1];
  316. return call.Method.DeclaringType.IsKnownType(KnownTypeCode.Decimal);
  317. }
  318. return false;
  319. }
  320. /// <summary>
  321. /// Represents either non-lifted IL `Comp` or a call to one of the (non-lifted) 6 comparison operators on `System.Decimal`.
  322. /// </summary>
  323. struct CompOrDecimal
  324. {
  325. public ILInstruction Instruction;
  326. public ComparisonKind Kind;
  327. public ILInstruction Left;
  328. public ILInstruction Right;
  329. internal ILInstruction MakeLifted(ComparisonKind newComparisonKind, ILInstruction left, ILInstruction right)
  330. {
  331. if (Instruction is Comp comp) {
  332. return new Comp(newComparisonKind, ComparisonLiftingKind.CSharp, comp.InputType, comp.Sign, left, right) {
  333. ILRange = Instruction.ILRange
  334. };
  335. } else if (Instruction is Call call) {
  336. IMethod method;
  337. if (newComparisonKind == Kind) {
  338. method = call.Method;
  339. } else if (newComparisonKind == ComparisonKind.Inequality && call.Method.Name == "op_Equality") {
  340. method = call.Method.DeclaringType.GetMethods(m => m.Name == "op_Inequality")
  341. .FirstOrDefault(m => ParameterListComparer.Instance.Equals(m.Parameters, call.Method.Parameters));
  342. if (method == null)
  343. return null;
  344. } else {
  345. return null;
  346. }
  347. return new Call(CSharp.Resolver.CSharpOperators.LiftUserDefinedOperator(method)) {
  348. Arguments = { left, right },
  349. ConstrainedTo = call.ConstrainedTo,
  350. ILRange = call.ILRange,
  351. ILStackWasEmpty = call.ILStackWasEmpty,
  352. IsTail = call.IsTail
  353. };
  354. } else {
  355. return null;
  356. }
  357. }
  358. }
  359. #endregion
  360. #region Lift...Comparison
  361. ILInstruction LiftCSharpEqualityComparison(CompOrDecimal valueComp, ComparisonKind newComparisonKind, ILInstruction hasValueTest)
  362. {
  363. Debug.Assert(newComparisonKind.IsEqualityOrInequality());
  364. bool hasValueTestNegated = false;
  365. while (hasValueTest.MatchLogicNot(out var arg)) {
  366. hasValueTest = arg;
  367. hasValueTestNegated = !hasValueTestNegated;
  368. }
  369. // The HasValue comparison must be the same operator as the Value comparison.
  370. if (hasValueTest is Comp hasValueComp) {
  371. // Comparing two nullables: HasValue comparison must be the same operator as the Value comparison
  372. if ((hasValueTestNegated ? hasValueComp.Kind.Negate() : hasValueComp.Kind) != newComparisonKind)
  373. return null;
  374. if (!MatchHasValueCall(hasValueComp.Left, out var leftVar))
  375. return null;
  376. if (!MatchHasValueCall(hasValueComp.Right, out var rightVar))
  377. return null;
  378. nullableVars = new List<ILVariable> { leftVar };
  379. var (left, leftBits) = DoLift(valueComp.Left);
  380. nullableVars[0] = rightVar;
  381. var (right, rightBits) = DoLift(valueComp.Right);
  382. if (left != null && right != null && leftBits[0] && rightBits[0]
  383. && SemanticHelper.IsPure(left.Flags) && SemanticHelper.IsPure(right.Flags)
  384. ) {
  385. context.Step("NullableLiftingTransform: C# (in)equality comparison", valueComp.Instruction);
  386. return valueComp.MakeLifted(newComparisonKind, left, right);
  387. }
  388. } else if (newComparisonKind == ComparisonKind.Equality && !hasValueTestNegated && MatchHasValueCall(hasValueTest, out var v)) {
  389. // Comparing nullable with non-nullable -> we can fall back to the normal comparison code.
  390. nullableVars = new List<ILVariable> { v };
  391. return LiftCSharpComparison(valueComp, newComparisonKind);
  392. } else if (newComparisonKind == ComparisonKind.Inequality && hasValueTestNegated && MatchHasValueCall(hasValueTest, out v)) {
  393. // Comparing nullable with non-nullable -> we can fall back to the normal comparison code.
  394. nullableVars = new List<ILVariable> { v };
  395. return LiftCSharpComparison(valueComp, newComparisonKind);
  396. }
  397. return null;
  398. }
  399. /// <summary>
  400. /// Lift a C# comparison.
  401. /// This method cannot be used for (in)equality comparisons where both sides are nullable
  402. /// (these special cases are handled in LiftCSharpEqualityComparison instead).
  403. ///
  404. /// The output instructions should evaluate to <c>false</c> when any of the <c>nullableVars</c> is <c>null</c>
  405. /// (except for newComparisonKind==Inequality, where this case should evaluate to <c>true</c> instead).
  406. /// Otherwise, the output instruction should evaluate to the same value as the input instruction.
  407. /// The output instruction should have the same side-effects (incl. exceptions being thrown) as the input instruction.
  408. /// This means unlike LiftNormal(), we cannot rely on the input instruction not being evaluated if
  409. /// a variable is <c>null</c>.
  410. /// </summary>
  411. ILInstruction LiftCSharpComparison(CompOrDecimal comp, ComparisonKind newComparisonKind)
  412. {
  413. var (left, right, bits) = DoLiftBinary(comp.Left, comp.Right);
  414. // due to the restrictions on side effects, we only allow instructions that are pure after lifting.
  415. // (we can't check this before lifting due to the calls to GetValueOrDefault())
  416. if (left != null && right != null && SemanticHelper.IsPure(left.Flags) && SemanticHelper.IsPure(right.Flags)) {
  417. if (!bits.All(0, nullableVars.Count)) {
  418. // don't lift if a nullableVar doesn't contribute to the result
  419. return null;
  420. }
  421. context.Step("NullableLiftingTransform: C# comparison", comp.Instruction);
  422. return comp.MakeLifted(newComparisonKind, left, right);
  423. }
  424. return null;
  425. }
  426. Call LiftCSharpUserEqualityComparison(CompOrDecimal hasValueComp, ComparisonKind newComparisonKind, ILInstruction nestedIfInst)
  427. {
  428. // User-defined equality operator:
  429. // if (comp(call get_HasValue(ldloca nullable1) == call get_HasValue(ldloca nullable2)))
  430. // if (logic.not(call get_HasValue(ldloca nullable)))
  431. // ldc.i4 1
  432. // else
  433. // call op_Equality(call GetValueOrDefault(ldloca nullable1), call GetValueOrDefault(ldloca nullable2)
  434. // else
  435. // ldc.i4 0
  436. // User-defined inequality operator:
  437. // if (comp(call get_HasValue(ldloca nullable1) != call get_HasValue(ldloca nullable2)))
  438. // ldc.i4 1
  439. // else
  440. // if (call get_HasValue(ldloca nullable))
  441. // call op_Inequality(call GetValueOrDefault(ldloca nullable1), call GetValueOrDefault(ldloca nullable2))
  442. // else
  443. // ldc.i4 0
  444. if (!MatchHasValueCall(hasValueComp.Left, out var nullable1))
  445. return null;
  446. if (!MatchHasValueCall(hasValueComp.Right, out var nullable2))
  447. return null;
  448. if (!nestedIfInst.MatchIfInstructionPositiveCondition(out var condition, out var trueInst, out var falseInst))
  449. return null;
  450. if (!MatchHasValueCall(condition, out var nullable))
  451. return null;
  452. if (nullable != nullable1 && nullable != nullable2)
  453. return null;
  454. if (!falseInst.MatchLdcI4(newComparisonKind == ComparisonKind.Equality ? 1 : 0))
  455. return null;
  456. if (!(trueInst is Call call))
  457. return null;
  458. if (!(call.Method.IsOperator && call.Arguments.Count == 2))
  459. return null;
  460. if (call.Method.Name != (newComparisonKind == ComparisonKind.Equality ? "op_Equality" : "op_Inequality"))
  461. return null;
  462. var liftedOperator = CSharp.Resolver.CSharpOperators.LiftUserDefinedOperator(call.Method);
  463. if (liftedOperator == null)
  464. return null;
  465. nullableVars = new List<ILVariable> { nullable1 };
  466. var (left, leftBits) = DoLift(call.Arguments[0]);
  467. nullableVars[0] = nullable2;
  468. var (right, rightBits) = DoLift(call.Arguments[1]);
  469. if (left != null && right != null && leftBits[0] && rightBits[0]
  470. && SemanticHelper.IsPure(left.Flags) && SemanticHelper.IsPure(right.Flags)
  471. ) {
  472. context.Step("NullableLiftingTransform: C# user-defined (in)equality comparison", nestedIfInst);
  473. return new Call(liftedOperator) {
  474. Arguments = { left, right },
  475. ConstrainedTo = call.ConstrainedTo,
  476. ILRange = call.ILRange,
  477. ILStackWasEmpty = call.ILStackWasEmpty,
  478. IsTail = call.IsTail,
  479. };
  480. }
  481. return null;
  482. }
  483. #endregion
  484. #region LiftNormal / DoLift
  485. /// <summary>
  486. /// Performs nullable lifting.
  487. ///
  488. /// Produces a lifted instruction with semantics equivalent to:
  489. /// (v1 != null && ... && vn != null) ? trueInst : falseInst,
  490. /// where the v1,...,vn are the <c>this.nullableVars</c>.
  491. /// If lifting fails, returns <c>null</c>.
  492. /// </summary>
  493. ILInstruction LiftNormal(ILInstruction trueInst, ILInstruction falseInst, Interval ilrange)
  494. {
  495. if (trueInst.MatchIfInstructionPositiveCondition(out var nestedCondition, out var nestedTrue, out var nestedFalse)) {
  496. // Sometimes Roslyn generates pointless conditions like:
  497. // if (nullable.HasValue && (!nullable.HasValue || nullable.GetValueOrDefault() == b))
  498. if (MatchHasValueCall(nestedCondition, out var v) && nullableVars.Contains(v)) {
  499. trueInst = nestedTrue;
  500. }
  501. }
  502. bool isNullCoalescingWithNonNullableFallback = false;
  503. if (!MatchNullableCtor(trueInst, out var utype, out var exprToLift)) {
  504. isNullCoalescingWithNonNullableFallback = true;
  505. utype = context.TypeSystem.Compilation.FindType(trueInst.ResultType.ToKnownTypeCode());
  506. exprToLift = trueInst;
  507. if (nullableVars.Count == 1 && exprToLift.MatchLdLoc(nullableVars[0])) {
  508. // v.HasValue ? ldloc v : fallback
  509. // => v ?? fallback
  510. context.Step("v.HasValue ? v : fallback => v ?? fallback", trueInst);
  511. return new NullCoalescingInstruction(NullCoalescingKind.Nullable, trueInst, falseInst) {
  512. UnderlyingResultType = NullableType.GetUnderlyingType(nullableVars[0].Type).GetStackType(),
  513. ILRange = ilrange
  514. };
  515. } else if (trueInst is Call call && !call.IsLifted
  516. && CSharp.Resolver.CSharpOperators.IsComparisonOperator(call.Method)
  517. && falseInst.MatchLdcI4(call.Method.Name == "op_Inequality" ? 1 : 0))
  518. {
  519. // (v1 != null && ... && vn != null) ? call op_LessThan(lhs, rhs) : ldc.i4(0)
  520. var liftedOperator = CSharp.Resolver.CSharpOperators.LiftUserDefinedOperator(call.Method);
  521. if ((call.Method.Name == "op_Equality" || call.Method.Name == "op_Inequality") && nullableVars.Count != 1) {
  522. // Equality is special (returns true if both sides are null), only handle it
  523. // in the normal code path if we're dealing with only a single nullable var
  524. // (comparing nullable with non-nullable).
  525. liftedOperator = null;
  526. }
  527. if (liftedOperator != null) {
  528. context.Step("Lift user-defined comparison operator", trueInst);
  529. var (left, right, bits) = DoLiftBinary(call.Arguments[0], call.Arguments[1]);
  530. if (left != null && right != null && bits.All(0, nullableVars.Count)) {
  531. return new Call(liftedOperator) {
  532. Arguments = { left, right },
  533. ConstrainedTo = call.ConstrainedTo,
  534. ILRange = call.ILRange,
  535. ILStackWasEmpty = call.ILStackWasEmpty,
  536. IsTail = call.IsTail
  537. };
  538. }
  539. }
  540. }
  541. }
  542. ILInstruction lifted;
  543. if (nullableVars.Count == 1 && MatchGetValueOrDefault(exprToLift, nullableVars[0])) {
  544. // v.HasValue ? call GetValueOrDefault(ldloca v) : fallback
  545. // => conv.nop.lifted(ldloc v) ?? fallback
  546. // This case is handled separately from DoLift() because
  547. // that doesn't introduce nop-conversions.
  548. context.Step("v.HasValue ? v.GetValueOrDefault() : fallback => v ?? fallback", trueInst);
  549. var inputUType = NullableType.GetUnderlyingType(nullableVars[0].Type);
  550. lifted = new LdLoc(nullableVars[0]);
  551. if (!inputUType.Equals(utype) && utype.ToPrimitiveType() != PrimitiveType.None) {
  552. // While the ILAst allows implicit conversions between short and int
  553. // (because both map to I4); it does not allow implicit conversions
  554. // between short? and int? (structs of different types).
  555. // So use 'conv.nop.lifted' to allow the conversion.
  556. lifted = new Conv(
  557. lifted,
  558. inputUType.GetStackType(), inputUType.GetSign(), utype.ToPrimitiveType(),
  559. checkForOverflow: false,
  560. isLifted: true
  561. ) {
  562. ILRange = ilrange
  563. };
  564. }
  565. } else {
  566. context.Step("NullableLiftingTransform.DoLift", trueInst);
  567. BitSet bits;
  568. (lifted, bits) = DoLift(exprToLift);
  569. if (lifted == null) {
  570. return null;
  571. }
  572. if (!bits.All(0, nullableVars.Count)) {
  573. // don't lift if a nullableVar doesn't contribute to the result
  574. return null;
  575. }
  576. Debug.Assert(lifted is ILiftableInstruction liftable && liftable.IsLifted
  577. && liftable.UnderlyingResultType == exprToLift.ResultType);
  578. }
  579. if (isNullCoalescingWithNonNullableFallback) {
  580. lifted = new NullCoalescingInstruction(NullCoalescingKind.NullableWithValueFallback, lifted, falseInst) {
  581. UnderlyingResultType = exprToLift.ResultType,
  582. ILRange = ilrange
  583. };
  584. } else if (!MatchNull(falseInst, utype)) {
  585. // Normal lifting, but the falseInst isn't `default(utype?)`
  586. // => use the `??` operator to provide the fallback value.
  587. lifted = new NullCoalescingInstruction(NullCoalescingKind.Nullable, lifted, falseInst) {
  588. UnderlyingResultType = exprToLift.ResultType,
  589. ILRange = ilrange
  590. };
  591. }
  592. return lifted;
  593. }
  594. /// <summary>
  595. /// Recursive function that lifts the specified instruction.
  596. /// The input instruction is expected to a subexpression of the trueInst
  597. /// (so that all nullableVars are guaranteed non-null within this expression).
  598. ///
  599. /// Creates a new lifted instruction without modifying the input instruction.
  600. /// On success, returns (new lifted instruction, bitset).
  601. /// If lifting fails, returns (null, null).
  602. ///
  603. /// The returned bitset specifies which nullableVars were considered "relevant" for this instruction.
  604. /// bitSet[i] == true means nullableVars[i] was relevant.
  605. ///
  606. /// The new lifted instruction will have equivalent semantics to the input instruction
  607. /// if all relevant variables are non-null [except that the result will be wrapped in a Nullable{T} struct].
  608. /// If any relevant variable is null, the new instruction is guaranteed to evaluate to <c>null</c>
  609. /// without having any other effect.
  610. /// </summary>
  611. (ILInstruction, BitSet) DoLift(ILInstruction inst)
  612. {
  613. if (MatchGetValueOrDefault(inst, out ILVariable inputVar)) {
  614. // n.GetValueOrDefault() lifted => n.
  615. BitSet foundIndices = new BitSet(nullableVars.Count);
  616. for (int i = 0; i < nullableVars.Count; i++) {
  617. if (nullableVars[i] == inputVar) {
  618. foundIndices[i] = true;
  619. }
  620. }
  621. if (foundIndices.Any())
  622. return (new LdLoc(inputVar) { ILRange = inst.ILRange }, foundIndices);
  623. else
  624. return (null, null);
  625. } else if (inst is Conv conv) {
  626. var (arg, bits) = DoLift(conv.Argument);
  627. if (arg != null) {
  628. if (conv.HasDirectFlag(InstructionFlags.MayThrow) && !bits.All(0, nullableVars.Count)) {
  629. // Cannot execute potentially-throwing instruction unless all
  630. // the nullableVars are arguments to the instruction
  631. // (thus causing it not to throw when any of them is null).
  632. return (null, null);
  633. }
  634. var newInst = new Conv(arg, conv.InputType, conv.InputSign, conv.TargetType, conv.CheckForOverflow, isLifted: true) {
  635. ILRange = conv.ILRange
  636. };
  637. return (newInst, bits);
  638. }
  639. } else if (inst is BitNot bitnot) {
  640. var (arg, bits) = DoLift(bitnot.Argument);
  641. if (arg != null) {
  642. var newInst = new BitNot(arg, isLifted: true, stackType: bitnot.ResultType) {
  643. ILRange = bitnot.ILRange
  644. };
  645. return (newInst, bits);
  646. }
  647. } else if (inst is BinaryNumericInstruction binary) {
  648. var (left, right, bits) = DoLiftBinary(binary.Left, binary.Right);
  649. if (left != null && right != null) {
  650. if (binary.HasDirectFlag(InstructionFlags.MayThrow) && !bits.All(0, nullableVars.Count)) {
  651. // Cannot execute potentially-throwing instruction unless all
  652. // the nullableVars are arguments to the instruction
  653. // (thus causing it not to throw when any of them is null).
  654. return (null, null);
  655. }
  656. var newInst = new BinaryNumericInstruction(
  657. binary.Operator, left, right,
  658. binary.LeftInputType, binary.RightInputType,
  659. binary.CheckForOverflow, binary.Sign,
  660. isLifted: true
  661. ) {
  662. ILRange = binary.ILRange
  663. };
  664. return (newInst, bits);
  665. }
  666. } else if (inst is Comp comp && !comp.IsLifted && comp.Kind == ComparisonKind.Equality
  667. && MatchGetValueOrDefault(comp.Left, out ILVariable v)
  668. && NullableType.GetUnderlyingType(v.Type).IsKnownType(KnownTypeCode.Boolean)
  669. && comp.Right.MatchLdcI4(0)
  670. ) {
  671. // C# doesn't support ComparisonLiftingKind.ThreeValuedLogic,
  672. // except for operator! on bool?.
  673. var (arg, bits) = DoLift(comp.Left);
  674. Debug.Assert(arg != null);
  675. var newInst = new Comp(comp.Kind, ComparisonLiftingKind.ThreeValuedLogic, comp.InputType, comp.Sign, arg, comp.Right.Clone()) {
  676. ILRange = comp.ILRange
  677. };
  678. return (newInst, bits);
  679. } else if (inst is Call call && call.Method.IsOperator) {
  680. // Lifted user-defined operators, except for comparison operators (as those return bool, not bool?)
  681. var liftedOperator = CSharp.Resolver.CSharpOperators.LiftUserDefinedOperator(call.Method);
  682. if (liftedOperator == null || !NullableType.IsNullable(liftedOperator.ReturnType))
  683. return (null, null);
  684. ILInstruction[] newArgs;
  685. BitSet newBits;
  686. if (call.Arguments.Count == 1) {
  687. var (arg, bits) = DoLift(call.Arguments[0]);
  688. newArgs = new[] { arg };
  689. newBits = bits;
  690. } else if (call.Arguments.Count == 2) {
  691. var (left, right, bits) = DoLiftBinary(call.Arguments[0], call.Arguments[1]);
  692. newArgs = new[] { left, right };
  693. newBits = bits;
  694. } else {
  695. return (null, null);
  696. }
  697. if (newBits == null || !newBits.All(0, nullableVars.Count)) {
  698. // all nullable vars must be involved when calling a method (side effect)
  699. return (null, null);
  700. }
  701. var newInst = new Call(liftedOperator) {
  702. ConstrainedTo = call.ConstrainedTo,
  703. IsTail = call.IsTail,
  704. ILStackWasEmpty = call.ILStackWasEmpty,
  705. ILRange = call.ILRange
  706. };
  707. newInst.Arguments.AddRange(newArgs);
  708. return (newInst, newBits);
  709. }
  710. return (null, null);
  711. }
  712. (ILInstruction, ILInstruction, BitSet) DoLiftBinary(ILInstruction lhs, ILInstruction rhs)
  713. {
  714. var (left, leftBits) = DoLift(lhs);
  715. var (right, rightBits) = DoLift(rhs);
  716. if (left != null && right == null && SemanticHelper.IsPure(rhs.Flags)) {
  717. // Embed non-nullable pure expression in lifted expression.
  718. right = rhs.Clone();
  719. }
  720. if (left == null && right != null && SemanticHelper.IsPure(lhs.Flags)) {
  721. // Embed non-nullable pure expression in lifted expression.
  722. left = lhs.Clone();
  723. }
  724. if (left != null && right != null) {
  725. var bits = leftBits ?? rightBits;
  726. if (rightBits != null)
  727. bits.UnionWith(rightBits);
  728. return (left, right, bits);
  729. } else {
  730. return (null, null, null);
  731. }
  732. }
  733. #endregion
  734. #region Match...Call
  735. /// <summary>
  736. /// Matches 'call get_HasValue(ldloca v)'
  737. /// </summary>
  738. internal static bool MatchHasValueCall(ILInstruction inst, out ILVariable v)
  739. {
  740. v = null;
  741. if (!(inst is Call call))
  742. return false;
  743. if (call.Arguments.Count != 1)
  744. return false;
  745. if (call.Method.Name != "get_HasValue")
  746. return false;
  747. if (call.Method.DeclaringTypeDefinition?.KnownTypeCode != KnownTypeCode.NullableOfT)
  748. return false;
  749. return call.Arguments[0].MatchLdLoca(out v);
  750. }
  751. /// <summary>
  752. /// Matches 'call get_HasValue(ldloca v)'
  753. /// </summary>
  754. internal static bool MatchHasValueCall(ILInstruction inst, ILVariable v)
  755. {
  756. return MatchHasValueCall(inst, out var v2) && v == v2;
  757. }
  758. /// <summary>
  759. /// Matches 'logic.not(call get_HasValue(ldloca v))'
  760. /// </summary>
  761. static bool MatchNegatedHasValueCall(ILInstruction inst, ILVariable v)
  762. {
  763. return inst.MatchLogicNot(out var arg) && MatchHasValueCall(arg, v);
  764. }
  765. /// <summary>
  766. /// Matches 'newobj Nullable{underlyingType}.ctor(arg)'
  767. /// </summary>
  768. static bool MatchNullableCtor(ILInstruction inst, out IType underlyingType, out ILInstruction arg)
  769. {
  770. underlyingType = null;
  771. arg = null;
  772. if (!(inst is NewObj newobj))
  773. return false;
  774. if (!newobj.Method.IsConstructor || newobj.Arguments.Count != 1)
  775. return false;
  776. if (newobj.Method.DeclaringTypeDefinition?.KnownTypeCode != KnownTypeCode.NullableOfT)
  777. return false;
  778. arg = newobj.Arguments[0];
  779. underlyingType = NullableType.GetUnderlyingType(newobj.Method.DeclaringType);
  780. return true;
  781. }
  782. /// <summary>
  783. /// Matches 'call Nullable{T}.GetValueOrDefault(arg)'
  784. /// </summary>
  785. static bool MatchGetValueOrDefault(ILInstruction inst, out ILInstruction arg)
  786. {
  787. arg = null;
  788. if (!(inst is Call call))
  789. return false;
  790. if (call.Method.Name != "GetValueOrDefault" || call.Arguments.Count != 1)
  791. return false;
  792. if (call.Method.DeclaringTypeDefinition?.KnownTypeCode != KnownTypeCode.NullableOfT)
  793. return false;
  794. arg = call.Arguments[0];
  795. return true;
  796. }
  797. /// <summary>
  798. /// Matches 'call Nullable{T}.GetValueOrDefault(ldloca v)'
  799. /// </summary>
  800. internal static bool MatchGetValueOrDefault(ILInstruction inst, out ILVariable v)
  801. {
  802. v = null;
  803. return MatchGetValueOrDefault(inst, out ILInstruction arg)
  804. && arg.MatchLdLoca(out v);
  805. }
  806. /// <summary>
  807. /// Matches 'call Nullable{T}.GetValueOrDefault(ldloca v)'
  808. /// </summary>
  809. internal static bool MatchGetValueOrDefault(ILInstruction inst, ILVariable v)
  810. {
  811. return MatchGetValueOrDefault(inst, out ILVariable v2) && v == v2;
  812. }
  813. static bool MatchNull(ILInstruction inst, out IType underlyingType)
  814. {
  815. underlyingType = null;
  816. if (inst.MatchDefaultValue(out IType type)) {
  817. underlyingType = NullableType.GetUnderlyingType(type);
  818. return NullableType.IsNullable(type);
  819. }
  820. underlyingType = null;
  821. return false;
  822. }
  823. static bool MatchNull(ILInstruction inst, IType underlyingType)
  824. {
  825. return MatchNull(inst, out var utype) && utype.Equals(underlyingType);
  826. }
  827. #endregion
  828. }
  829. class NullableLiftingStatementTransform : IStatementTransform
  830. {
  831. public void Run(Block block, int pos, StatementTransformContext context)
  832. {
  833. new NullableLiftingTransform(context).RunStatements(block, pos);
  834. }
  835. }
  836. }