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.

1037 lines
41 KiB

  1. // Copyright (c) 2015 Siegfried Pammer
  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.Diagnostics;
  20. using System.Linq;
  21. using System.Linq.Expressions;
  22. using ICSharpCode.Decompiler.CSharp;
  23. using ICSharpCode.Decompiler.TypeSystem;
  24. using ICSharpCode.Decompiler.Util;
  25. namespace ICSharpCode.Decompiler.IL.Transforms
  26. {
  27. /// <summary>
  28. /// Constructs compound assignments and inline assignments.
  29. /// </summary>
  30. /// <remarks>
  31. /// This is a statement transform;
  32. /// but some portions are executed as an expression transform instead
  33. /// (with HandleCompoundAssign() as entry point)
  34. /// </remarks>
  35. public class TransformAssignment : IStatementTransform
  36. {
  37. StatementTransformContext context;
  38. void IStatementTransform.Run(Block block, int pos, StatementTransformContext context)
  39. {
  40. this.context = context;
  41. if (context.Settings.MakeAssignmentExpressions)
  42. {
  43. if (TransformInlineAssignmentStObjOrCall(block, pos) || TransformInlineAssignmentLocal(block, pos))
  44. {
  45. // both inline assignments create a top-level stloc which might affect inlining
  46. context.RequestRerun();
  47. return;
  48. }
  49. }
  50. if (context.Settings.IntroduceIncrementAndDecrement)
  51. {
  52. if (TransformPostIncDecOperatorWithInlineStore(block, pos)
  53. || TransformPostIncDecOperator(block, pos))
  54. {
  55. // again, new top-level stloc might need inlining:
  56. context.RequestRerun();
  57. return;
  58. }
  59. }
  60. }
  61. /// <code>
  62. /// stloc s(value)
  63. /// stloc l(ldloc s)
  64. /// stobj(..., ldloc s)
  65. /// where ... is pure and does not use s or l,
  66. /// and where neither the 'stloc s' nor the 'stobj' truncates
  67. /// -->
  68. /// stloc l(stobj (..., value))
  69. /// </code>
  70. /// e.g. used for inline assignment to instance field
  71. ///
  72. /// -or-
  73. ///
  74. /// <code>
  75. /// stloc s(value)
  76. /// stobj (..., ldloc s)
  77. /// where ... is pure and does not use s, and where the 'stobj' does not truncate
  78. /// -->
  79. /// stloc s(stobj (..., value))
  80. /// </code>
  81. /// e.g. used for inline assignment to static field
  82. ///
  83. /// -or-
  84. ///
  85. /// <code>
  86. /// stloc s(value)
  87. /// call set_Property(..., ldloc s)
  88. /// where the '...' arguments are pure and not using 's'
  89. /// -->
  90. /// stloc s(Block InlineAssign { call set_Property(..., stloc i(value)); final: ldloc i })
  91. /// new temporary 'i' has type of the property; transform only valid if 'stloc i' doesn't truncate
  92. /// </code>
  93. bool TransformInlineAssignmentStObjOrCall(Block block, int pos)
  94. {
  95. var inst = block.Instructions[pos] as StLoc;
  96. // in some cases it can be a compiler-generated local
  97. if (inst == null || (inst.Variable.Kind != VariableKind.StackSlot && inst.Variable.Kind != VariableKind.Local))
  98. return false;
  99. if (IsImplicitTruncation(inst.Value, inst.Variable.Type, context.TypeSystem))
  100. {
  101. // 'stloc s' is implicitly truncating the value
  102. return false;
  103. }
  104. ILVariable local;
  105. int nextPos;
  106. if (block.Instructions[pos + 1] is StLoc localStore)
  107. { // with extra local
  108. if (localStore.Variable.Kind != VariableKind.Local || !localStore.Value.MatchLdLoc(inst.Variable))
  109. return false;
  110. // if we're using an extra local, we'll delete "s", so check that that doesn't have any additional uses
  111. if (!(inst.Variable.IsSingleDefinition && inst.Variable.LoadCount == 2))
  112. return false;
  113. local = localStore.Variable;
  114. nextPos = pos + 2;
  115. }
  116. else
  117. {
  118. local = inst.Variable;
  119. localStore = null;
  120. nextPos = pos + 1;
  121. if (local.LoadCount == 1 && local.AddressCount == 0)
  122. {
  123. // inline assignment would produce a dead store in this case, which is ugly
  124. // and causes problems with the deconstruction transform.
  125. return false;
  126. }
  127. }
  128. if (block.Instructions[nextPos] is StObj stobj)
  129. {
  130. // unaligned.stobj cannot be inlined in C#
  131. if (stobj.UnalignedPrefix > 0)
  132. return false;
  133. if (!stobj.Value.MatchLdLoc(inst.Variable))
  134. return false;
  135. if (!SemanticHelper.IsPure(stobj.Target.Flags) || inst.Variable.IsUsedWithin(stobj.Target))
  136. return false;
  137. var pointerType = stobj.Target.InferType(context.TypeSystem);
  138. IType newType = stobj.Type;
  139. if (TypeUtils.IsCompatiblePointerTypeForMemoryAccess(pointerType, stobj.Type))
  140. {
  141. if (pointerType is ByReferenceType byref)
  142. newType = byref.ElementType;
  143. else if (pointerType is PointerType pointer)
  144. newType = pointer.ElementType;
  145. }
  146. var truncation = CheckImplicitTruncation(inst.Value, newType, context.TypeSystem);
  147. if (truncation == ImplicitTruncationResult.ValueChanged)
  148. {
  149. // 'stobj' is implicitly truncating the value
  150. return false;
  151. }
  152. if (truncation == ImplicitTruncationResult.ValueChangedDueToSignMismatch)
  153. {
  154. // Change the sign of the type to skip implicit truncation
  155. newType = SwapSign(newType, context.TypeSystem);
  156. }
  157. context.Step("Inline assignment stobj", stobj);
  158. stobj.Type = newType;
  159. block.Instructions.Remove(localStore);
  160. block.Instructions.Remove(stobj);
  161. stobj.Value = inst.Value;
  162. inst.ReplaceWith(new StLoc(local, stobj));
  163. // note: our caller will trigger a re-run, which will call HandleStObjCompoundAssign if applicable
  164. return true;
  165. }
  166. else if (block.Instructions[nextPos] is CallInstruction call)
  167. {
  168. // call must be a setter call:
  169. if (!(call.OpCode == OpCode.Call || call.OpCode == OpCode.CallVirt))
  170. return false;
  171. if (call.ResultType != StackType.Void || call.Arguments.Count == 0)
  172. return false;
  173. IProperty property = call.Method.AccessorOwner as IProperty;
  174. if (property == null)
  175. return false;
  176. if (!call.Method.Equals(property.Setter))
  177. return false;
  178. if (!(property.IsIndexer || property.Setter.Parameters.Count == 1))
  179. {
  180. // this is a parameterized property, which cannot be expressed as C# code.
  181. // setter calls are not valid in expression context, if property syntax cannot be used.
  182. return false;
  183. }
  184. if (!call.Arguments.Last().MatchLdLoc(inst.Variable))
  185. return false;
  186. foreach (var arg in call.Arguments.SkipLast(1))
  187. {
  188. if (!SemanticHelper.IsPure(arg.Flags) || inst.Variable.IsUsedWithin(arg))
  189. return false;
  190. }
  191. if (IsImplicitTruncation(inst.Value, call.Method.Parameters.Last().Type, context.TypeSystem))
  192. {
  193. // setter call is implicitly truncating the value
  194. return false;
  195. }
  196. // stloc s(Block InlineAssign { call set_Property(..., stloc i(value)); final: ldloc i })
  197. context.Step("Inline assignment call", call);
  198. block.Instructions.Remove(localStore);
  199. block.Instructions.Remove(call);
  200. var newVar = context.Function.RegisterVariable(VariableKind.StackSlot, call.Method.Parameters.Last().Type);
  201. call.Arguments[call.Arguments.Count - 1] = new StLoc(newVar, inst.Value);
  202. var inlineBlock = new Block(BlockKind.CallInlineAssign) {
  203. Instructions = { call },
  204. FinalInstruction = new LdLoc(newVar)
  205. };
  206. inst.ReplaceWith(new StLoc(local, inlineBlock));
  207. // because the ExpressionTransforms don't look into inline blocks, manually trigger HandleCallCompoundAssign
  208. if (HandleCompoundAssign(call, context))
  209. {
  210. // if we did construct a compound assignment, it should have made our inline block redundant:
  211. Debug.Assert(!inlineBlock.IsConnected);
  212. }
  213. return true;
  214. }
  215. else
  216. {
  217. return false;
  218. }
  219. }
  220. private static IType SwapSign(IType type, ICompilation compilation)
  221. {
  222. return type.ToPrimitiveType() switch {
  223. PrimitiveType.I1 => compilation.FindType(KnownTypeCode.Byte),
  224. PrimitiveType.I2 => compilation.FindType(KnownTypeCode.UInt16),
  225. PrimitiveType.I4 => compilation.FindType(KnownTypeCode.UInt32),
  226. PrimitiveType.I8 => compilation.FindType(KnownTypeCode.UInt64),
  227. PrimitiveType.U1 => compilation.FindType(KnownTypeCode.SByte),
  228. PrimitiveType.U2 => compilation.FindType(KnownTypeCode.Int16),
  229. PrimitiveType.U4 => compilation.FindType(KnownTypeCode.Int32),
  230. PrimitiveType.U8 => compilation.FindType(KnownTypeCode.Int64),
  231. PrimitiveType.I => compilation.FindType(KnownTypeCode.UIntPtr),
  232. PrimitiveType.U => compilation.FindType(KnownTypeCode.IntPtr),
  233. _ => throw new ArgumentException("Type must have an opposing sign: " + type, nameof(type))
  234. };
  235. }
  236. static ILInstruction UnwrapSmallIntegerConv(ILInstruction inst, out Conv conv)
  237. {
  238. conv = inst as Conv;
  239. if (conv != null && conv.Kind == ConversionKind.Truncate && conv.TargetType.IsSmallIntegerType())
  240. {
  241. // for compound assignments to small integers, the compiler emits a "conv" instruction
  242. return conv.Argument;
  243. }
  244. else
  245. {
  246. return inst;
  247. }
  248. }
  249. static bool ValidateCompoundAssign(BinaryNumericInstruction binary, Conv conv, IType targetType, DecompilerSettings settings)
  250. {
  251. if (!NumericCompoundAssign.IsBinaryCompatibleWithType(binary, targetType, settings))
  252. return false;
  253. if (conv != null && !(conv.TargetType == targetType.ToPrimitiveType() && conv.CheckForOverflow == binary.CheckForOverflow))
  254. return false; // conv does not match binary operation
  255. return true;
  256. }
  257. static bool MatchingGetterAndSetterCalls(CallInstruction getterCall, CallInstruction setterCall, out Action<ILTransformContext> finalizeMatch)
  258. {
  259. finalizeMatch = null;
  260. if (getterCall == null || setterCall == null || !IsSameMember(getterCall.Method.AccessorOwner, setterCall.Method.AccessorOwner))
  261. return false;
  262. if (setterCall.OpCode != getterCall.OpCode)
  263. return false;
  264. var owner = getterCall.Method.AccessorOwner as IProperty;
  265. if (owner == null || !IsSameMember(getterCall.Method, owner.Getter) || !IsSameMember(setterCall.Method, owner.Setter))
  266. return false;
  267. if (setterCall.Arguments.Count != getterCall.Arguments.Count + 1)
  268. return false;
  269. // Ensure that same arguments are passed to getterCall and setterCall:
  270. for (int j = 0; j < getterCall.Arguments.Count; j++)
  271. {
  272. if (setterCall.Arguments[j].MatchStLoc(out var v) && v.IsSingleDefinition && v.LoadCount == 1)
  273. {
  274. if (getterCall.Arguments[j].MatchLdLoc(v))
  275. {
  276. // OK, setter call argument is saved in temporary that is re-used for getter call
  277. if (finalizeMatch == null)
  278. {
  279. finalizeMatch = AdjustArguments;
  280. }
  281. continue;
  282. }
  283. }
  284. if (!SemanticHelper.IsPure(getterCall.Arguments[j].Flags))
  285. return false;
  286. if (!getterCall.Arguments[j].Match(setterCall.Arguments[j]).Success)
  287. return false;
  288. }
  289. return true;
  290. void AdjustArguments(ILTransformContext context)
  291. {
  292. Debug.Assert(setterCall.Arguments.Count == getterCall.Arguments.Count + 1);
  293. for (int j = 0; j < getterCall.Arguments.Count; j++)
  294. {
  295. if (setterCall.Arguments[j].MatchStLoc(out var v, out var value))
  296. {
  297. Debug.Assert(v.IsSingleDefinition && v.LoadCount == 1);
  298. Debug.Assert(getterCall.Arguments[j].MatchLdLoc(v));
  299. getterCall.Arguments[j] = value;
  300. }
  301. }
  302. }
  303. }
  304. /// <summary>
  305. /// Transform compound assignments where the return value is not being used,
  306. /// or where there's an inlined assignment within the setter call.
  307. ///
  308. /// Patterns handled:
  309. /// 1.
  310. /// callvirt set_Property(ldloc S_1, binary.op(callvirt get_Property(ldloc S_1), value))
  311. /// ==> compound.op.new(callvirt get_Property(ldloc S_1), value)
  312. /// 2.
  313. /// callvirt set_Property(ldloc S_1, stloc v(binary.op(callvirt get_Property(ldloc S_1), value)))
  314. /// ==> stloc v(compound.op.new(callvirt get_Property(ldloc S_1), value))
  315. /// 3.
  316. /// stobj(target, binary.op(ldobj(target), ...))
  317. /// where target is pure
  318. /// => compound.op(target, ...)
  319. /// </summary>
  320. /// <remarks>
  321. /// Called by ExpressionTransforms, or after the inline-assignment transform for setters.
  322. /// </remarks>
  323. internal static bool HandleCompoundAssign(ILInstruction compoundStore, StatementTransformContext context)
  324. {
  325. if (!context.Settings.MakeAssignmentExpressions || !context.Settings.IntroduceIncrementAndDecrement)
  326. return false;
  327. if (compoundStore is CallInstruction && compoundStore.SlotInfo != Block.InstructionSlot)
  328. {
  329. // replacing 'call set_Property' with a compound assignment instruction
  330. // changes the return value of the expression, so this is only valid on block-level.
  331. return false;
  332. }
  333. if (!IsCompoundStore(compoundStore, out var targetType, out var setterValue, context.TypeSystem))
  334. return false;
  335. // targetType = The type of the property/field/etc. being stored to.
  336. // setterValue = The value being stored.
  337. var storeInSetter = setterValue as StLoc;
  338. if (storeInSetter != null)
  339. {
  340. // We'll move the stloc to top-level:
  341. // callvirt set_Property(ldloc S_1, stloc v(binary.op(callvirt get_Property(ldloc S_1), value)))
  342. // ==> stloc v(compound.op.new(callvirt get_Property(ldloc S_1), value))
  343. setterValue = storeInSetter.Value;
  344. if (storeInSetter.Variable.Type.IsSmallIntegerType())
  345. {
  346. // 'stloc v' implicitly truncates the value.
  347. // Ensure that type of 'v' matches the type of the property:
  348. if (storeInSetter.Variable.Type.GetSize() != targetType.GetSize())
  349. return false;
  350. if (storeInSetter.Variable.Type.GetSign() != targetType.GetSign())
  351. return false;
  352. }
  353. }
  354. ILInstruction newInst;
  355. if (UnwrapSmallIntegerConv(setterValue, out var smallIntConv) is BinaryNumericInstruction binary)
  356. {
  357. if (compoundStore is StLoc)
  358. {
  359. // transform local variables only for user-defined operators
  360. return false;
  361. }
  362. if (!IsMatchingCompoundLoad(binary.Left, compoundStore, out var target, out var targetKind, out var finalizeMatch, forbiddenVariable: storeInSetter?.Variable))
  363. return false;
  364. if (!ValidateCompoundAssign(binary, smallIntConv, targetType, context.Settings))
  365. return false;
  366. context.Step($"Compound assignment (binary.numeric)", compoundStore);
  367. finalizeMatch?.Invoke(context);
  368. newInst = new NumericCompoundAssign(
  369. binary, target, targetKind, binary.Right,
  370. targetType, CompoundEvalMode.EvaluatesToNewValue);
  371. }
  372. else if (setterValue is Call operatorCall && operatorCall.Method.IsOperator)
  373. {
  374. if (operatorCall.Arguments.Count == 0)
  375. return false;
  376. if (!IsMatchingCompoundLoad(operatorCall.Arguments[0], compoundStore, out var target, out var targetKind, out var finalizeMatch, forbiddenVariable: storeInSetter?.Variable))
  377. return false;
  378. ILInstruction rhs;
  379. if (operatorCall.Arguments.Count == 2)
  380. {
  381. if (CSharp.ExpressionBuilder.GetAssignmentOperatorTypeFromMetadataName(operatorCall.Method.Name, context.Settings) == null)
  382. return false;
  383. rhs = operatorCall.Arguments[1];
  384. }
  385. else if (operatorCall.Arguments.Count == 1)
  386. {
  387. if (!UserDefinedCompoundAssign.IsIncrementOrDecrement(operatorCall.Method, context.Settings))
  388. return false;
  389. // use a dummy node so that we don't need a dedicated instruction for user-defined unary operator calls
  390. rhs = new LdcI4(1);
  391. }
  392. else
  393. {
  394. return false;
  395. }
  396. if (operatorCall.IsLifted)
  397. return false; // TODO: add tests and think about whether nullables need special considerations
  398. context.Step($"Compound assignment (user-defined binary)", compoundStore);
  399. finalizeMatch?.Invoke(context);
  400. newInst = new UserDefinedCompoundAssign(operatorCall.Method, CompoundEvalMode.EvaluatesToNewValue,
  401. target, targetKind, rhs);
  402. }
  403. else if (setterValue is DynamicBinaryOperatorInstruction dynamicBinaryOp)
  404. {
  405. if (!IsMatchingCompoundLoad(dynamicBinaryOp.Left, compoundStore, out var target, out var targetKind, out var finalizeMatch, forbiddenVariable: storeInSetter?.Variable))
  406. return false;
  407. context.Step($"Compound assignment (dynamic binary)", compoundStore);
  408. finalizeMatch?.Invoke(context);
  409. newInst = new DynamicCompoundAssign(ToCompound(dynamicBinaryOp.Operation), dynamicBinaryOp.BinderFlags, target, dynamicBinaryOp.LeftArgumentInfo, dynamicBinaryOp.Right, dynamicBinaryOp.RightArgumentInfo, targetKind);
  410. static ExpressionType ToCompound(ExpressionType from)
  411. {
  412. return from switch {
  413. ExpressionType.Add => ExpressionType.AddAssign,
  414. ExpressionType.AddChecked => ExpressionType.AddAssignChecked,
  415. ExpressionType.And => ExpressionType.AndAssign,
  416. ExpressionType.Divide => ExpressionType.DivideAssign,
  417. ExpressionType.ExclusiveOr => ExpressionType.ExclusiveOrAssign,
  418. ExpressionType.LeftShift => ExpressionType.LeftShiftAssign,
  419. ExpressionType.Modulo => ExpressionType.ModuloAssign,
  420. ExpressionType.Multiply => ExpressionType.MultiplyAssign,
  421. ExpressionType.MultiplyChecked => ExpressionType.MultiplyAssignChecked,
  422. ExpressionType.Or => ExpressionType.OrAssign,
  423. ExpressionType.Power => ExpressionType.PowerAssign,
  424. ExpressionType.RightShift => ExpressionType.RightShiftAssign,
  425. ExpressionType.Subtract => ExpressionType.SubtractAssign,
  426. ExpressionType.SubtractChecked => ExpressionType.SubtractAssignChecked,
  427. _ => from
  428. };
  429. }
  430. }
  431. else if (setterValue is Call concatCall && UserDefinedCompoundAssign.IsStringConcat(concatCall.Method))
  432. {
  433. // setterValue is a string.Concat() invocation
  434. if (compoundStore is StLoc)
  435. {
  436. // transform local variables only for user-defined operators
  437. return false;
  438. }
  439. if (concatCall.Arguments.Count != 2)
  440. return false; // for now we only support binary compound assignments
  441. if (!targetType.IsKnownType(KnownTypeCode.String))
  442. return false;
  443. var arg = concatCall.Arguments[0];
  444. if (arg is Call call && CallBuilder.IsStringToReadOnlySpanCharImplicitConversion(call.Method))
  445. {
  446. arg = call.Arguments[0];
  447. if (!(concatCall.Arguments[1] is NewObj { Arguments: [AddressOf addressOf] } newObj) || !ILInlining.IsReadOnlySpanCharCtor(newObj.Method))
  448. {
  449. return false;
  450. }
  451. }
  452. if (!IsMatchingCompoundLoad(arg, compoundStore, out var target, out var targetKind, out var finalizeMatch, forbiddenVariable: storeInSetter?.Variable))
  453. return false;
  454. context.Step($"Compound assignment (string concatenation)", compoundStore);
  455. finalizeMatch?.Invoke(context);
  456. newInst = new UserDefinedCompoundAssign(concatCall.Method, CompoundEvalMode.EvaluatesToNewValue,
  457. target, targetKind, concatCall.Arguments[1]);
  458. }
  459. else
  460. {
  461. return false;
  462. }
  463. newInst.AddILRange(setterValue);
  464. if (storeInSetter != null)
  465. {
  466. storeInSetter.Value = newInst;
  467. newInst = storeInSetter;
  468. context.RequestRerun(); // moving stloc to top-level might trigger inlining
  469. }
  470. compoundStore.ReplaceWith(newInst);
  471. if (newInst.Parent is Block inlineAssignBlock && inlineAssignBlock.Kind == BlockKind.CallInlineAssign)
  472. {
  473. // It's possible that we first replaced the instruction in an inline-assign helper block.
  474. // In such a situation, we know from the block invariant that we're have a storeInSetter.
  475. Debug.Assert(storeInSetter != null);
  476. Debug.Assert(storeInSetter.Variable.IsSingleDefinition && storeInSetter.Variable.LoadCount == 1);
  477. Debug.Assert(inlineAssignBlock.Instructions.Single() == storeInSetter);
  478. Debug.Assert(inlineAssignBlock.FinalInstruction.MatchLdLoc(storeInSetter.Variable));
  479. // Block CallInlineAssign { stloc I_0(compound.op(...)); final: ldloc I_0 }
  480. // --> compound.op(...)
  481. inlineAssignBlock.ReplaceWith(storeInSetter.Value);
  482. }
  483. return true;
  484. }
  485. /// <code>
  486. /// stloc s(value)
  487. /// stloc l(ldloc s)
  488. /// where neither 'stloc s' nor 'stloc l' truncates the value
  489. /// -->
  490. /// stloc s(stloc l(value))
  491. /// </code>
  492. bool TransformInlineAssignmentLocal(Block block, int pos)
  493. {
  494. var inst = block.Instructions[pos] as StLoc;
  495. var nextInst = block.Instructions.ElementAtOrDefault(pos + 1) as StLoc;
  496. if (inst == null || nextInst == null)
  497. return false;
  498. if (inst.Variable.Kind != VariableKind.StackSlot)
  499. return false;
  500. if (!(nextInst.Variable.Kind == VariableKind.Local || nextInst.Variable.Kind == VariableKind.Parameter))
  501. return false;
  502. if (!nextInst.Value.MatchLdLoc(inst.Variable))
  503. return false;
  504. if (IsImplicitTruncation(inst.Value, inst.Variable.Type, context.TypeSystem))
  505. {
  506. // 'stloc s' is implicitly truncating the stack value
  507. return false;
  508. }
  509. if (IsImplicitTruncation(inst.Value, nextInst.Variable.Type, context.TypeSystem))
  510. {
  511. // 'stloc l' is implicitly truncating the stack value
  512. return false;
  513. }
  514. if (nextInst.Variable.StackType == StackType.Ref)
  515. {
  516. // ref locals need to be initialized when they are declared, so
  517. // we can only use inline assignments when we know that the
  518. // ref local is definitely assigned.
  519. // We don't have an easy way to check for that in this transform,
  520. // so avoid inline assignments to ref locals for now.
  521. return false;
  522. }
  523. context.Step("Inline assignment to local variable", inst);
  524. var value = inst.Value;
  525. var var = nextInst.Variable;
  526. var stackVar = inst.Variable;
  527. block.Instructions.RemoveAt(pos);
  528. nextInst.ReplaceWith(new StLoc(stackVar, new StLoc(var, value)));
  529. return true;
  530. }
  531. internal static bool IsImplicitTruncation(ILInstruction value, IType type, ICompilation compilation, bool allowNullableValue = false)
  532. {
  533. return CheckImplicitTruncation(value, type, compilation, allowNullableValue) != ImplicitTruncationResult.ValuePreserved;
  534. }
  535. internal enum ImplicitTruncationResult : byte
  536. {
  537. /// <summary>
  538. /// The value is not implicitly truncated.
  539. /// </summary>
  540. ValuePreserved,
  541. /// <summary>
  542. /// The value is implicitly truncated.
  543. /// </summary>
  544. ValueChanged,
  545. /// <summary>
  546. /// The value is implicitly truncated, but the sign of the target type can be changed to remove the truncation.
  547. /// </summary>
  548. ValueChangedDueToSignMismatch
  549. }
  550. /// <summary>
  551. /// Gets whether 'stobj type(..., value)' would evaluate to a different value than 'value'
  552. /// due to implicit truncation.
  553. /// </summary>
  554. internal static ImplicitTruncationResult CheckImplicitTruncation(ILInstruction value, IType type, ICompilation compilation, bool allowNullableValue = false)
  555. {
  556. if (!type.IsSmallIntegerType())
  557. {
  558. // Implicit truncation in ILAst only happens for small integer types;
  559. // other types of implicit truncation in IL cause the ILReader to insert
  560. // conv instructions.
  561. return ImplicitTruncationResult.ValuePreserved;
  562. }
  563. // With small integer types, test whether the value might be changed by
  564. // truncation (based on type.GetSize()) followed by sign/zero extension (based on type.GetSign()).
  565. // (it's OK to have false-positives here if we're unsure)
  566. if (value.MatchLdcI4(out int val))
  567. {
  568. bool valueFits = (type.GetEnumUnderlyingType().GetDefinition()?.KnownTypeCode) switch {
  569. KnownTypeCode.Boolean => val == 0 || val == 1,
  570. KnownTypeCode.Byte => val >= byte.MinValue && val <= byte.MaxValue,
  571. KnownTypeCode.SByte => val >= sbyte.MinValue && val <= sbyte.MaxValue,
  572. KnownTypeCode.Int16 => val >= short.MinValue && val <= short.MaxValue,
  573. KnownTypeCode.UInt16 or KnownTypeCode.Char => val >= ushort.MinValue && val <= ushort.MaxValue,
  574. _ => false
  575. };
  576. return valueFits ? ImplicitTruncationResult.ValuePreserved : ImplicitTruncationResult.ValueChanged;
  577. }
  578. else if (value is Conv conv)
  579. {
  580. PrimitiveType primitiveType = type.ToPrimitiveType();
  581. PrimitiveType convTargetType = conv.TargetType;
  582. if (convTargetType == primitiveType)
  583. return ImplicitTruncationResult.ValuePreserved;
  584. if (primitiveType.GetSize() == convTargetType.GetSize() && primitiveType.GetSign() != convTargetType.GetSign() && primitiveType.HasOppositeSign())
  585. return ImplicitTruncationResult.ValueChangedDueToSignMismatch;
  586. return ImplicitTruncationResult.ValueChanged;
  587. }
  588. else if (value is Comp)
  589. {
  590. return ImplicitTruncationResult.ValuePreserved; // comp returns 0 or 1, which always fits
  591. }
  592. else if (value is BinaryNumericInstruction bni)
  593. {
  594. switch (bni.Operator)
  595. {
  596. case BinaryNumericOperator.BitAnd:
  597. case BinaryNumericOperator.BitOr:
  598. case BinaryNumericOperator.BitXor:
  599. // If both input values fit into the type without truncation,
  600. // the result of a binary operator will also fit.
  601. var leftTruncation = CheckImplicitTruncation(bni.Left, type, compilation, allowNullableValue);
  602. // If the left side is truncating and a sign change is not possible we do not need to evaluate the right side
  603. if (leftTruncation == ImplicitTruncationResult.ValueChanged)
  604. return ImplicitTruncationResult.ValueChanged;
  605. var rightTruncation = CheckImplicitTruncation(bni.Right, type, compilation, allowNullableValue);
  606. return CommonImplicitTruncation(leftTruncation, rightTruncation);
  607. }
  608. }
  609. else if (value is IfInstruction ifInst)
  610. {
  611. var trueTruncation = CheckImplicitTruncation(ifInst.TrueInst, type, compilation, allowNullableValue);
  612. // If the true branch is truncating and a sign change is not possible we do not need to evaluate the false branch
  613. if (trueTruncation == ImplicitTruncationResult.ValueChanged)
  614. return ImplicitTruncationResult.ValueChanged;
  615. var falseTruncation = CheckImplicitTruncation(ifInst.FalseInst, type, compilation, allowNullableValue);
  616. return CommonImplicitTruncation(trueTruncation, falseTruncation);
  617. }
  618. else
  619. {
  620. IType inferredType = value.InferType(compilation);
  621. if (allowNullableValue)
  622. {
  623. inferredType = NullableType.GetUnderlyingType(inferredType);
  624. }
  625. if (inferredType.Kind != TypeKind.Unknown)
  626. {
  627. var inferredPrimitive = inferredType.ToPrimitiveType();
  628. var primitiveType = type.ToPrimitiveType();
  629. bool sameSign = inferredPrimitive.GetSign() == primitiveType.GetSign();
  630. if (inferredPrimitive.GetSize() <= primitiveType.GetSize() && sameSign)
  631. return ImplicitTruncationResult.ValuePreserved;
  632. if (inferredPrimitive.GetSize() == primitiveType.GetSize() && !sameSign && primitiveType.HasOppositeSign())
  633. return ImplicitTruncationResult.ValueChangedDueToSignMismatch;
  634. return ImplicitTruncationResult.ValueChanged;
  635. }
  636. }
  637. // In unknown cases, assume that the value might be changed by truncation.
  638. return ImplicitTruncationResult.ValueChanged;
  639. }
  640. private static ImplicitTruncationResult CommonImplicitTruncation(ImplicitTruncationResult left, ImplicitTruncationResult right)
  641. {
  642. if (left == right)
  643. return left;
  644. // Note: in all cases where left!=right, we return ValueChanged:
  645. // if only one side can be fixed by changing the sign, we don't want to change the sign of the other side.
  646. return ImplicitTruncationResult.ValueChanged;
  647. }
  648. /// <summary>
  649. /// Gets whether 'inst' is a possible store for use as a compound store.
  650. /// </summary>
  651. /// <remarks>
  652. /// Output parameters:
  653. /// storeType: The type of the value being stored.
  654. /// value: The value being stored (will be analyzed further to detect compound assignments)
  655. ///
  656. /// Every IsCompoundStore() call should be followed by an IsMatchingCompoundLoad() call.
  657. /// </remarks>
  658. static bool IsCompoundStore(ILInstruction inst, out IType storeType,
  659. out ILInstruction value, ICompilation compilation)
  660. {
  661. value = null;
  662. storeType = null;
  663. if (inst is StObj stobj)
  664. {
  665. // stobj.Type may just be 'int' (due to stind.i4) when we're actually operating on a 'ref MyEnum'.
  666. // Try to determine the real type of the object we're modifying:
  667. storeType = stobj.Target.InferType(compilation);
  668. if (storeType is ByReferenceType refType)
  669. {
  670. if (TypeUtils.IsCompatibleTypeForMemoryAccess(refType.ElementType, stobj.Type))
  671. {
  672. storeType = refType.ElementType;
  673. }
  674. else
  675. {
  676. storeType = stobj.Type;
  677. }
  678. }
  679. else if (storeType is PointerType pointerType)
  680. {
  681. if (TypeUtils.IsCompatibleTypeForMemoryAccess(pointerType.ElementType, stobj.Type))
  682. {
  683. storeType = pointerType.ElementType;
  684. }
  685. else
  686. {
  687. storeType = stobj.Type;
  688. }
  689. }
  690. else
  691. {
  692. storeType = stobj.Type;
  693. }
  694. value = stobj.Value;
  695. return SemanticHelper.IsPure(stobj.Target.Flags);
  696. }
  697. else if (inst is CallInstruction call && (call.OpCode == OpCode.Call || call.OpCode == OpCode.CallVirt))
  698. {
  699. if (call.Method.Parameters.Count == 0)
  700. {
  701. return false;
  702. }
  703. foreach (var arg in call.Arguments.SkipLast(1))
  704. {
  705. if (arg.MatchStLoc(out var v) && v.IsSingleDefinition && v.LoadCount == 1)
  706. {
  707. continue; // OK, IsMatchingCompoundLoad can perform an adjustment in this special case
  708. }
  709. if (!SemanticHelper.IsPure(arg.Flags))
  710. {
  711. return false;
  712. }
  713. }
  714. storeType = call.Method.Parameters.Last().Type;
  715. value = call.Arguments.Last();
  716. return IsSameMember(call.Method, (call.Method.AccessorOwner as IProperty)?.Setter);
  717. }
  718. else if (inst is StLoc stloc && (stloc.Variable.Kind == VariableKind.Local || stloc.Variable.Kind == VariableKind.Parameter))
  719. {
  720. storeType = stloc.Variable.Type;
  721. value = stloc.Value;
  722. return true;
  723. }
  724. else
  725. {
  726. return false;
  727. }
  728. }
  729. /// <summary>
  730. /// Checks whether 'load' and 'store' both access the same store, and can be combined to a compound assignment.
  731. /// </summary>
  732. /// <param name="load">The load instruction to test.</param>
  733. /// <param name="store">The compound store to test against. Must have previously been tested via IsCompoundStore()</param>
  734. /// <param name="target">The target to use for the compound assignment instruction.</param>
  735. /// <param name="targetKind">The target kind to use for the compound assignment instruction.</param>
  736. /// <param name="finalizeMatch">If set to a non-null value, call this delegate to fix up minor mismatches between getter and setter.</param>
  737. /// <param name="forbiddenVariable">
  738. /// If given a non-null value, this function returns false if the forbiddenVariable is used in the load/store instructions.
  739. /// Some transforms effectively move a store around,
  740. /// which is only valid if the variable stored to does not occur in the compound load/store.
  741. /// </param>
  742. /// <param name="previousInstruction">
  743. /// Instruction preceding the load.
  744. /// </param>
  745. static bool IsMatchingCompoundLoad(ILInstruction load, ILInstruction store,
  746. out ILInstruction target, out CompoundTargetKind targetKind,
  747. out Action<ILTransformContext> finalizeMatch,
  748. ILVariable forbiddenVariable = null,
  749. ILInstruction previousInstruction = null)
  750. {
  751. target = null;
  752. targetKind = 0;
  753. finalizeMatch = null;
  754. if (load is LdObj ldobj && store is StObj stobj)
  755. {
  756. Debug.Assert(SemanticHelper.IsPure(stobj.Target.Flags));
  757. if (!SemanticHelper.IsPure(ldobj.Target.Flags))
  758. return false;
  759. if (forbiddenVariable != null && forbiddenVariable.IsUsedWithin(ldobj.Target))
  760. return false;
  761. target = ldobj.Target;
  762. targetKind = CompoundTargetKind.Address;
  763. if (ldobj.Target.Match(stobj.Target).Success)
  764. {
  765. return true;
  766. }
  767. else if (IsDuplicatedAddressComputation(stobj.Target, ldobj.Target))
  768. {
  769. // Use S_0 as target, so that S_0 can later be eliminated by inlining.
  770. // (we can't eliminate previousInstruction right now, because it's before the transform's starting instruction)
  771. target = stobj.Target;
  772. return true;
  773. }
  774. else
  775. {
  776. return false;
  777. }
  778. }
  779. else if (MatchingGetterAndSetterCalls(load as CallInstruction, store as CallInstruction, out finalizeMatch))
  780. {
  781. if (forbiddenVariable != null && forbiddenVariable.IsUsedWithin(load))
  782. return false;
  783. target = load;
  784. targetKind = CompoundTargetKind.Property;
  785. return true;
  786. }
  787. else if (load is LdLoc ldloc && store is StLoc stloc && ILVariableEqualityComparer.Instance.Equals(ldloc.Variable, stloc.Variable))
  788. {
  789. if (ILVariableEqualityComparer.Instance.Equals(ldloc.Variable, forbiddenVariable))
  790. return false;
  791. target = new LdLoca(ldloc.Variable).WithILRange(ldloc);
  792. targetKind = CompoundTargetKind.Address;
  793. finalizeMatch = context => context.Function.RecombineVariables(ldloc.Variable, stloc.Variable);
  794. return true;
  795. }
  796. else
  797. {
  798. return false;
  799. }
  800. bool IsDuplicatedAddressComputation(ILInstruction storeTarget, ILInstruction loadTarget)
  801. {
  802. // Sometimes roslyn duplicates the address calculation:
  803. // stloc S_0(ldloc refParam)
  804. // stloc V_0(ldobj System.Int32(ldloc refParam))
  805. // stobj System.Int32(ldloc S_0, binary.add.i4(ldloc V_0, ldc.i4 1))
  806. while (storeTarget is LdFlda storeLdFlda && loadTarget is LdFlda loadLdFlda)
  807. {
  808. if (!storeLdFlda.Field.Equals(loadLdFlda.Field))
  809. return false;
  810. storeTarget = storeLdFlda.Target;
  811. loadTarget = loadLdFlda.Target;
  812. }
  813. if (!storeTarget.MatchLdLoc(out var s))
  814. return false;
  815. if (!(s.Kind == VariableKind.StackSlot && s.IsSingleDefinition && s != forbiddenVariable))
  816. return false;
  817. if (s.StoreInstructions.SingleOrDefault() != previousInstruction)
  818. return false;
  819. return previousInstruction is StLoc addressStore && addressStore.Value.Match(loadTarget).Success;
  820. }
  821. }
  822. /// <code>
  823. /// stobj(target, binary.add(stloc l(ldobj(target)), ldc.i4 1))
  824. /// where target is pure and does not use 'l', and the 'stloc l' does not truncate
  825. /// -->
  826. /// stloc l(compound.op.old(ldobj(target), ldc.i4 1))
  827. ///
  828. /// -or-
  829. ///
  830. /// call set_Prop(args..., binary.add(stloc l(call get_Prop(args...)), ldc.i4 1))
  831. /// where args.. are pure and do not use 'l', and the 'stloc l' does not truncate
  832. /// -->
  833. /// stloc l(compound.op.old(call get_Prop(target), ldc.i4 1))
  834. /// </code>
  835. /// <remarks>
  836. /// This pattern is used for post-increment by legacy csc.
  837. ///
  838. /// Even though this transform operates only on a single expression, it's not an expression transform
  839. /// as the result value of the expression changes (this is OK only for statements in a block).
  840. /// </remarks>
  841. bool TransformPostIncDecOperatorWithInlineStore(Block block, int pos)
  842. {
  843. var store = block.Instructions[pos];
  844. if (!IsCompoundStore(store, out var targetType, out var value, context.TypeSystem))
  845. {
  846. return false;
  847. }
  848. StLoc stloc;
  849. var binary = UnwrapSmallIntegerConv(value, out var conv) as BinaryNumericInstruction;
  850. if (binary != null && (binary.Right.MatchLdcI(1) || binary.Right.MatchLdcF4(1) || binary.Right.MatchLdcF8(1)))
  851. {
  852. if (!(binary.Operator == BinaryNumericOperator.Add || binary.Operator == BinaryNumericOperator.Sub))
  853. return false;
  854. if (conv is not null)
  855. {
  856. var primitiveType = targetType.ToPrimitiveType();
  857. if (primitiveType.GetSize() == conv.TargetType.GetSize() && primitiveType.GetSign() != conv.TargetType.GetSign())
  858. targetType = SwapSign(targetType, context.TypeSystem);
  859. }
  860. if (!ValidateCompoundAssign(binary, conv, targetType, context.Settings))
  861. return false;
  862. stloc = binary.Left as StLoc;
  863. }
  864. else if (value is Call operatorCall && operatorCall.Method.IsOperator && operatorCall.Arguments.Count == 1)
  865. {
  866. if (!(operatorCall.Method.Name == "op_Increment" || operatorCall.Method.Name == "op_Decrement"))
  867. return false;
  868. if (operatorCall.IsLifted)
  869. return false; // TODO: add tests and think about whether nullables need special considerations
  870. stloc = operatorCall.Arguments[0] as StLoc;
  871. }
  872. else
  873. {
  874. return false;
  875. }
  876. if (stloc == null)
  877. return false;
  878. if (!(stloc.Variable.Kind == VariableKind.Local || stloc.Variable.Kind == VariableKind.StackSlot))
  879. return false;
  880. if (!IsMatchingCompoundLoad(stloc.Value, store, out var target, out var targetKind, out var finalizeMatch, forbiddenVariable: stloc.Variable))
  881. return false;
  882. if (IsImplicitTruncation(stloc.Value, stloc.Variable.Type, context.TypeSystem))
  883. return false;
  884. context.Step("TransformPostIncDecOperatorWithInlineStore", store);
  885. finalizeMatch?.Invoke(context);
  886. if (binary != null)
  887. {
  888. block.Instructions[pos] = new StLoc(stloc.Variable, new NumericCompoundAssign(
  889. binary, target, targetKind, binary.Right, targetType, CompoundEvalMode.EvaluatesToOldValue));
  890. }
  891. else
  892. {
  893. Call operatorCall = (Call)value;
  894. block.Instructions[pos] = new StLoc(stloc.Variable, new UserDefinedCompoundAssign(
  895. operatorCall.Method, CompoundEvalMode.EvaluatesToOldValue, target, targetKind, new LdcI4(1)));
  896. }
  897. return true;
  898. }
  899. /// <code>
  900. /// stloc tmp(ldobj(target))
  901. /// stobj(target, binary.op(ldloc tmp, ldc.i4 1))
  902. /// target is pure and does not use 'tmp', 'stloc does not truncate'
  903. /// -->
  904. /// stloc tmp(compound.op.old(ldobj(target), ldc.i4 1))
  905. /// </code>
  906. /// This is usually followed by inlining or eliminating 'tmp'.
  907. ///
  908. /// Local variables use a similar pattern, also detected by this function:
  909. /// <code>
  910. /// stloc tmp(ldloc target)
  911. /// stloc target(binary.op(ldloc tmp, ldc.i4 1))
  912. /// -->
  913. /// stloc tmp(compound.op.old(ldloca target, ldc.i4 1))
  914. /// </code>
  915. /// <remarks>
  916. /// This pattern occurs with legacy csc for static fields, and with Roslyn for most post-increments.
  917. /// </remarks>
  918. bool TransformPostIncDecOperator(Block block, int i)
  919. {
  920. var inst = block.Instructions[i] as StLoc;
  921. var store = block.Instructions.ElementAtOrDefault(i + 1);
  922. if (inst == null || store == null)
  923. return false;
  924. var tmpVar = inst.Variable;
  925. if (!IsCompoundStore(store, out var targetType, out var value, context.TypeSystem))
  926. return false;
  927. var truncation = CheckImplicitTruncation(inst.Value, targetType, context.TypeSystem);
  928. if (truncation == ImplicitTruncationResult.ValueChanged)
  929. {
  930. // 'stloc tmp' is implicitly truncating the value
  931. return false;
  932. }
  933. if (truncation == ImplicitTruncationResult.ValueChangedDueToSignMismatch)
  934. {
  935. if (!(store is StObj stObj && stObj.Type.Equals(targetType)))
  936. {
  937. // We cannot apply the sign change, so we can't fix the truncation
  938. return false;
  939. }
  940. }
  941. if (!IsMatchingCompoundLoad(inst.Value, store, out var target, out var targetKind, out var finalizeMatch,
  942. forbiddenVariable: inst.Variable,
  943. previousInstruction: block.Instructions.ElementAtOrDefault(i - 1)))
  944. {
  945. return false;
  946. }
  947. if (UnwrapSmallIntegerConv(value, out var conv) is BinaryNumericInstruction binary)
  948. {
  949. if (!(binary.Operator == BinaryNumericOperator.Add || binary.Operator == BinaryNumericOperator.Sub))
  950. return false;
  951. if (!binary.Left.MatchLdLoc(tmpVar))
  952. return false;
  953. if (targetType is PointerType ptrType)
  954. {
  955. var right = PointerArithmeticOffset.Detect(binary.Right, ptrType.ElementType, binary.CheckForOverflow);
  956. if (right is null || !right.MatchLdcI(1))
  957. return false;
  958. }
  959. else if (!(binary.Right.MatchLdcI(1) || binary.Right.MatchLdcF4(1) || binary.Right.MatchLdcF8(1)))
  960. return false;
  961. if (truncation == ImplicitTruncationResult.ValueChangedDueToSignMismatch && store is StObj stObj)
  962. {
  963. // Change the sign of the type to skip implicit truncation
  964. stObj.Type = targetType = SwapSign(targetType, context.TypeSystem);
  965. }
  966. if (!ValidateCompoundAssign(binary, conv, targetType, context.Settings))
  967. return false;
  968. context.Step("TransformPostIncDecOperator (builtin)", inst);
  969. finalizeMatch?.Invoke(context);
  970. inst.Value = new NumericCompoundAssign(binary, target, targetKind, binary.Right,
  971. targetType, CompoundEvalMode.EvaluatesToOldValue);
  972. }
  973. else if (value is Call operatorCall && operatorCall.Method.IsOperator && operatorCall.Arguments.Count == 1)
  974. {
  975. if (!operatorCall.Arguments[0].MatchLdLoc(tmpVar))
  976. return false;
  977. if (!UserDefinedCompoundAssign.IsIncrementOrDecrement(operatorCall.Method, context.Settings))
  978. return false;
  979. if (operatorCall.IsLifted)
  980. return false; // TODO: add tests and think about whether nullables need special considerations
  981. context.Step("TransformPostIncDecOperator (user-defined)", inst);
  982. Debug.Assert(truncation == ImplicitTruncationResult.ValuePreserved);
  983. finalizeMatch?.Invoke(context);
  984. inst.Value = new UserDefinedCompoundAssign(operatorCall.Method,
  985. CompoundEvalMode.EvaluatesToOldValue, target, targetKind, new LdcI4(1));
  986. }
  987. else
  988. {
  989. return false;
  990. }
  991. block.Instructions.RemoveAt(i + 1);
  992. if (inst.Variable.IsSingleDefinition && inst.Variable.LoadCount == 0)
  993. {
  994. // dead store -> it was a statement-level post-increment
  995. inst.ReplaceWith(inst.Value);
  996. }
  997. return true;
  998. }
  999. static bool IsSameMember(IMember a, IMember b)
  1000. {
  1001. if (a == null || b == null)
  1002. return false;
  1003. a = a.MemberDefinition;
  1004. b = b.MemberDefinition;
  1005. return a.Equals(b);
  1006. }
  1007. }
  1008. }