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.

1790 lines
66 KiB

7 years ago
  1. // Copyright (c) 2017 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.Collections.Generic;
  20. using System.Linq;
  21. using ICSharpCode.Decompiler.IL.ControlFlow;
  22. using ICSharpCode.Decompiler.TypeSystem;
  23. using ICSharpCode.Decompiler.Util;
  24. namespace ICSharpCode.Decompiler.IL.Transforms
  25. {
  26. using HashtableInitializer = Dictionary<IField, (List<(string, int)> Labels, IfInstruction JumpToNext, Block ContainingBlock, Block Previous, Block Next, bool Transformed)>;
  27. /// <summary>
  28. /// Detects switch-on-string patterns employed by the C# compiler and transforms them to an ILAst-switch-instruction.
  29. /// </summary>
  30. public class SwitchOnStringTransform : IILTransform
  31. {
  32. ILTransformContext context;
  33. private readonly SwitchAnalysis analysis = new SwitchAnalysis();
  34. public void Run(ILFunction function, ILTransformContext context)
  35. {
  36. if (!context.Settings.SwitchStatementOnString)
  37. return;
  38. this.context = context;
  39. BlockContainer body = (BlockContainer)function.Body;
  40. var hashtableInitializers = ScanHashtableInitializerBlocks(body.EntryPoint);
  41. HashSet<BlockContainer> changedContainers = new HashSet<BlockContainer>();
  42. foreach (var block in function.Descendants.OfType<Block>())
  43. {
  44. bool changed = false;
  45. if (block.IncomingEdgeCount == 0)
  46. continue;
  47. for (int i = block.Instructions.Count - 1; i >= 0; i--)
  48. {
  49. if (SimplifyCascadingIfStatements(block.Instructions, ref i))
  50. {
  51. changed = true;
  52. continue;
  53. }
  54. if (SimplifyCSharp1CascadingIfStatements(block.Instructions, ref i))
  55. {
  56. changed = true;
  57. continue;
  58. }
  59. if (MatchLegacySwitchOnStringWithHashtable(block, hashtableInitializers, ref i))
  60. {
  61. changed = true;
  62. continue;
  63. }
  64. if (MatchLegacySwitchOnStringWithDict(block.Instructions, ref i))
  65. {
  66. changed = true;
  67. continue;
  68. }
  69. if (MatchRoslynSwitchOnString(block.Instructions, ref i))
  70. {
  71. changed = true;
  72. continue;
  73. }
  74. if (MatchRoslynSwitchOnStringUsingLengthAndChar(block, i))
  75. {
  76. changed = true;
  77. continue;
  78. }
  79. }
  80. if (!changed)
  81. continue;
  82. SwitchDetection.SimplifySwitchInstruction(block, context);
  83. if (block.Parent is BlockContainer container)
  84. changedContainers.Add(container);
  85. }
  86. var omittedBlocks = new Dictionary<Block, Block>();
  87. // Remove all transformed hashtable initializers from the entrypoint.
  88. foreach (var item in hashtableInitializers)
  89. {
  90. var (labels, jumpToNext, containingBlock, previous, next, transformed) = item.Value;
  91. if (!transformed)
  92. continue;
  93. if (!omittedBlocks.TryGetValue(previous, out var actual))
  94. actual = previous;
  95. context.Step("Remove hashtable initializer", actual);
  96. if (jumpToNext != null)
  97. {
  98. actual.Instructions.SecondToLastOrDefault().ReplaceWith(jumpToNext);
  99. }
  100. actual.Instructions.LastOrDefault().ReplaceWith(new Branch(next));
  101. omittedBlocks.Add(containingBlock, previous);
  102. changedContainers.Add(body);
  103. }
  104. // If all initializer where removed, remove the initial null check as well.
  105. if (hashtableInitializers.Count > 0 && omittedBlocks.Count == hashtableInitializers.Count && body.EntryPoint.Instructions.Count == 2)
  106. {
  107. if (body.EntryPoint.Instructions[0] is IfInstruction ifInst
  108. && ifInst.TrueInst.MatchBranch(out var beginOfMethod) && body.EntryPoint.Instructions[1].MatchBranch(beginOfMethod))
  109. {
  110. context.Step("Remove initial null check", body);
  111. body.EntryPoint.Instructions.RemoveAt(0);
  112. }
  113. }
  114. foreach (var container in changedContainers)
  115. container.SortBlocks(deleteUnreachableBlocks: true);
  116. }
  117. HashtableInitializer ScanHashtableInitializerBlocks(Block entryPoint)
  118. {
  119. var hashtables = new HashtableInitializer();
  120. if (entryPoint.Instructions.Count != 2)
  121. return hashtables;
  122. // match first block: checking compiler-generated Hashtable for null
  123. // if (comp(volatile.ldobj System.Collections.Hashtable(ldsflda $$method0x600003f-1) != ldnull)) br switchHeadBlock
  124. // br tableInitBlock
  125. if (!(entryPoint.Instructions[0].MatchIfInstruction(out var condition, out var branchToSwitchHead)))
  126. return hashtables;
  127. if (!entryPoint.Instructions[1].MatchBranch(out var tableInitBlock))
  128. return hashtables;
  129. if (!(condition.MatchCompNotEquals(out var left, out var right) && right.MatchLdNull() &&
  130. MatchDictionaryFieldLoad(left, IsNonGenericHashtable, out var dictField, out var dictionaryType)))
  131. return hashtables;
  132. if (!branchToSwitchHead.MatchBranch(out var switchHead))
  133. return hashtables;
  134. // match second block: initialization of compiler-generated Hashtable
  135. // stloc table(newobj Hashtable..ctor(ldc.i4 capacity, ldc.f loadFactor))
  136. // call Add(ldloc table, ldstr value, box System.Int32(ldc.i4 index))
  137. // ... more calls to Add ...
  138. // volatile.stobj System.Collections.Hashtable(ldsflda $$method0x600003f - 1, ldloc table)
  139. // br switchHeadBlock
  140. if (tableInitBlock.IncomingEdgeCount != 1 || tableInitBlock.Instructions.Count < 3)
  141. return hashtables;
  142. Block previousBlock = entryPoint;
  143. while (tableInitBlock != null)
  144. {
  145. if (!ExtractStringValuesFromInitBlock(tableInitBlock, out var stringValues, out var blockAfterThisInitBlock, dictionaryType, dictField, true))
  146. break;
  147. var nextHashtableInitHead = tableInitBlock.Instructions.SecondToLastOrDefault() as IfInstruction;
  148. hashtables.Add(dictField, (stringValues, nextHashtableInitHead, tableInitBlock, previousBlock, blockAfterThisInitBlock, false));
  149. previousBlock = tableInitBlock;
  150. // if there is another IfInstruction before the end of the block, it might be a jump to the next hashtable init block.
  151. // if (comp(volatile.ldobj System.Collections.Hashtable(ldsflda $$method0x600003f-2) != ldnull)) br switchHeadBlock
  152. if (nextHashtableInitHead != null)
  153. {
  154. if (!(nextHashtableInitHead.Condition.MatchCompNotEquals(out left, out right) && right.MatchLdNull() &&
  155. MatchDictionaryFieldLoad(left, IsNonGenericHashtable, out var nextDictField, out _)))
  156. break;
  157. if (!nextHashtableInitHead.TrueInst.MatchBranch(switchHead))
  158. break;
  159. tableInitBlock = blockAfterThisInitBlock;
  160. dictField = nextDictField;
  161. }
  162. else
  163. {
  164. break;
  165. }
  166. }
  167. return hashtables;
  168. }
  169. bool SimplifyCascadingIfStatements(InstructionCollection<ILInstruction> instructions, ref int i)
  170. {
  171. // match first block: checking switch-value for null or first value (Roslyn)
  172. // if (call op_Equality(ldloc switchValueVar, ldstr value)) br firstBlock
  173. // -or-
  174. // if (comp(ldloc switchValueVar == ldnull)) br defaultBlock
  175. if (!instructions[i].MatchIfInstruction(out var condition, out var firstBlockOrDefaultJump))
  176. return false;
  177. var nextCaseJump = instructions[i + 1];
  178. while (condition.MatchLogicNot(out var arg))
  179. {
  180. condition = arg;
  181. ExtensionMethods.Swap(ref firstBlockOrDefaultJump, ref nextCaseJump);
  182. }
  183. // match call to operator ==(string, string)
  184. if (!MatchStringEqualityComparison(condition, out var switchValueVar, out string firstBlockValue, out bool isVBCompareString))
  185. return false;
  186. if (isVBCompareString)
  187. {
  188. ExtensionMethods.Swap(ref firstBlockOrDefaultJump, ref nextCaseJump);
  189. }
  190. if (firstBlockOrDefaultJump.MatchBranch(out var firstBlock))
  191. {
  192. // success
  193. }
  194. else if (firstBlockOrDefaultJump.MatchLeave(out _))
  195. {
  196. firstBlock = null;
  197. // success
  198. }
  199. else
  200. {
  201. return false;
  202. }
  203. var values = new List<(string, ILInstruction)>();
  204. var uniqueValues = new HashSet<string>();
  205. int numberOfUniqueMatchesWithCurrentVariable = 0;
  206. HashSet<Block> caseBlocks = new HashSet<Block>();
  207. caseBlocks.Add((Block)instructions[i].Parent);
  208. bool AddSwitchSection(string value, ILInstruction inst)
  209. {
  210. if (!uniqueValues.Add(value))
  211. return false;
  212. numberOfUniqueMatchesWithCurrentVariable++;
  213. values.Add((value, inst));
  214. return true;
  215. }
  216. ILInstruction switchValue = null;
  217. if (isVBCompareString && string.IsNullOrEmpty(firstBlockValue))
  218. {
  219. if (!AddSwitchSection(null, firstBlock ?? firstBlockOrDefaultJump))
  220. return false;
  221. if (!AddSwitchSection(string.Empty, firstBlock ?? firstBlockOrDefaultJump))
  222. return false;
  223. }
  224. else
  225. {
  226. if (!AddSwitchSection(firstBlockValue, firstBlock ?? firstBlockOrDefaultJump))
  227. return false;
  228. }
  229. bool removeExtraLoad = false;
  230. bool keepAssignmentBefore = false;
  231. if (i >= 1 && instructions[i - 1].MatchStLoc(switchValueVar, out switchValue))
  232. {
  233. // stloc switchValueVar(switchValue)
  234. // if (call op_Equality(ldloc switchValueVar, ldstr value)) br firstBlock
  235. // Newer versions of Roslyn use extra variables:
  236. if (i >= 2 && switchValue.MatchLdLoc(out var otherSwitchValueVar) && otherSwitchValueVar.IsSingleDefinition && otherSwitchValueVar.LoadCount == 1
  237. && instructions[i - 2].MatchStLoc(otherSwitchValueVar, out var newSwitchValue))
  238. {
  239. switchValue = newSwitchValue;
  240. removeExtraLoad = true;
  241. }
  242. }
  243. else if (i >= 1 && instructions[i - 1] is StLoc stloc)
  244. {
  245. if (stloc.Value.MatchLdLoc(switchValueVar))
  246. {
  247. // in case of optimized legacy code there are two stlocs:
  248. // stloc otherSwitchValueVar(ldloc switchValue)
  249. // stloc switchValueVar(ldloc otherSwitchValueVar)
  250. // if (call op_Equality(ldloc otherSwitchValueVar, ldstr value)) br firstBlock
  251. var otherSwitchValueVar = switchValueVar;
  252. switchValueVar = stloc.Variable;
  253. numberOfUniqueMatchesWithCurrentVariable = 0;
  254. if (i >= 2 && instructions[i - 2].MatchStLoc(otherSwitchValueVar, out switchValue)
  255. && otherSwitchValueVar.IsSingleDefinition && otherSwitchValueVar.LoadCount == 2)
  256. {
  257. removeExtraLoad = true;
  258. }
  259. else
  260. {
  261. switchValue = new LdLoc(otherSwitchValueVar);
  262. }
  263. }
  264. else
  265. {
  266. // Variable before the start of the switch is not related to the switch.
  267. keepAssignmentBefore = true;
  268. switchValue = new LdLoc(switchValueVar);
  269. }
  270. }
  271. else
  272. {
  273. // Instruction before the start of the switch is not related to the switch.
  274. keepAssignmentBefore = true;
  275. switchValue = new LdLoc(switchValueVar);
  276. }
  277. if (!switchValueVar.Type.IsKnownType(KnownTypeCode.String))
  278. {
  279. if (!context.Settings.SwitchOnReadOnlySpanChar)
  280. return false;
  281. if (!switchValueVar.Type.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
  282. && !switchValueVar.Type.IsKnownType(KnownTypeCode.SpanOfT))
  283. {
  284. return false;
  285. }
  286. }
  287. // if instruction must be followed by a branch to the next case
  288. if (!nextCaseJump.MatchBranch(out Block currentCaseBlock))
  289. return false;
  290. // extract all cases and add them to the values list.
  291. ILInstruction nextCaseBlock;
  292. do
  293. {
  294. nextCaseBlock = MatchCaseBlock(currentCaseBlock, switchValueVar, out string value, out bool emptyStringEqualsNull, out ILInstruction block);
  295. if (nextCaseBlock == null)
  296. break;
  297. if (emptyStringEqualsNull && string.IsNullOrEmpty(value))
  298. {
  299. if (!AddSwitchSection(null, block))
  300. return false;
  301. if (!AddSwitchSection(string.Empty, block))
  302. return false;
  303. }
  304. else
  305. {
  306. if (!AddSwitchSection(value, block))
  307. return false;
  308. }
  309. caseBlocks.Add(currentCaseBlock);
  310. currentCaseBlock = nextCaseBlock as Block;
  311. } while (currentCaseBlock != null);
  312. // We didn't find enough cases, exit
  313. if (values.Count < 3)
  314. return false;
  315. context.Step(nameof(SimplifyCascadingIfStatements), instructions[i]);
  316. // if the switchValueVar is used in other places as well, do not eliminate the store.
  317. if (switchValueVar.LoadCount > numberOfUniqueMatchesWithCurrentVariable || !ValidateUsesOfSwitchValueVariable(switchValueVar, caseBlocks))
  318. {
  319. keepAssignmentBefore = true;
  320. removeExtraLoad = false; // prevent loads from being deleted after detecting that
  321. // we have to keep the assignment before the switch statement
  322. switchValue = new LdLoc(switchValueVar);
  323. }
  324. int offset = firstBlock == null ? 1 : 0;
  325. var sections = new List<SwitchSection>(values.Skip(offset).SelectWithIndex((index, s) => new SwitchSection { Labels = new LongSet(index), Body = s.Item2 is Block b ? new Branch(b) : s.Item2.Clone() }));
  326. sections.Add(new SwitchSection { Labels = new LongSet(new LongInterval(0, sections.Count)).Invert(), Body = currentCaseBlock != null ? (ILInstruction)new Branch(currentCaseBlock) : new Leave((BlockContainer)nextCaseBlock) });
  327. var stringToInt = new StringToInt(switchValue, values.Skip(offset).Select(item => item.Item1).ToArray(), switchValueVar.Type);
  328. var inst = new SwitchInstruction(stringToInt);
  329. inst.Sections.AddRange(sections);
  330. if (removeExtraLoad)
  331. {
  332. inst.AddILRange(instructions[i - 2]);
  333. instructions[i - 2].ReplaceWith(inst);
  334. instructions.RemoveRange(i - 1, 3);
  335. i -= 2;
  336. }
  337. else
  338. {
  339. if (keepAssignmentBefore)
  340. {
  341. inst.AddILRange(instructions[i]);
  342. instructions[i].ReplaceWith(inst);
  343. instructions.RemoveAt(i + 1);
  344. }
  345. else
  346. {
  347. inst.AddILRange(instructions[i - 1]);
  348. instructions[i - 1].ReplaceWith(inst);
  349. instructions.RemoveRange(i, 2);
  350. i--;
  351. }
  352. }
  353. return true;
  354. }
  355. private bool ValidateUsesOfSwitchValueVariable(ILVariable switchValueVar, HashSet<Block> caseBlocks)
  356. {
  357. foreach (var use in switchValueVar.LoadInstructions)
  358. {
  359. bool isValid = false;
  360. foreach (var caseBlock in caseBlocks)
  361. {
  362. if (use.IsDescendantOf(caseBlock))
  363. isValid = true;
  364. }
  365. if (!isValid)
  366. return false;
  367. }
  368. return true;
  369. }
  370. bool SimplifyCSharp1CascadingIfStatements(InstructionCollection<ILInstruction> instructions, ref int i)
  371. {
  372. if (i < 1)
  373. return false;
  374. // match first block:
  375. // stloc switchValueVar(ldloc temp)
  376. // if (comp(ldloc temp == ldnull)) br defaultBlock
  377. // br isInternedBlock
  378. if (!(instructions[i].MatchIfInstruction(out var condition, out var defaultBlockJump)))
  379. return false;
  380. if (!instructions[i + 1].MatchBranch(out var isInternedBlock))
  381. return false;
  382. if (!defaultBlockJump.MatchBranch(out var defaultOrNullBlock))
  383. return false;
  384. if (!(condition.MatchCompEqualsNull(out var tempLoad) && tempLoad.MatchLdLoc(out var temp)))
  385. return false;
  386. if (!(temp.Kind == VariableKind.StackSlot && temp.LoadCount == 2))
  387. return false;
  388. if (!(instructions[i - 1].MatchStLoc(out var switchValueVar, out var switchValue) && switchValue.MatchLdLoc(temp)))
  389. return false;
  390. // match isInternedBlock:
  391. // stloc switchValueVarCopy(call IsInterned(ldloc switchValueVar))
  392. // if (comp(ldloc switchValueVarCopy == ldstr "case1")) br caseBlock1
  393. // br caseHeader2
  394. if (isInternedBlock.IncomingEdgeCount != 1 || isInternedBlock.Instructions.Count != 3)
  395. return false;
  396. if (!(isInternedBlock.Instructions[0].MatchStLoc(out var switchValueVarCopy, out var arg) && IsIsInternedCall(arg as Call, out arg) && arg.MatchLdLoc(switchValueVar)))
  397. return false;
  398. switchValueVar = switchValueVarCopy;
  399. int conditionOffset = 1;
  400. Block currentCaseBlock = isInternedBlock;
  401. var values = new List<(string, ILInstruction)>();
  402. if (!switchValueVarCopy.IsSingleDefinition)
  403. return false;
  404. // each case starts with:
  405. // if (comp(ldloc switchValueVar == ldstr "case label")) br caseBlock
  406. // br currentCaseBlock
  407. while (currentCaseBlock.Instructions[conditionOffset].MatchIfInstruction(out condition, out var caseBlockJump))
  408. {
  409. if (currentCaseBlock.Instructions.Count != conditionOffset + 2)
  410. break;
  411. if (!condition.MatchCompEquals(out var left, out var right))
  412. break;
  413. if (!left.MatchLdLoc(switchValueVar))
  414. break;
  415. if (!right.MatchLdStr(out string value))
  416. break;
  417. if (!(caseBlockJump.MatchBranch(out var caseBlock) || caseBlockJump.MatchLeave((BlockContainer)currentCaseBlock.Parent)))
  418. break;
  419. if (!currentCaseBlock.Instructions[conditionOffset + 1].MatchBranch(out currentCaseBlock))
  420. break;
  421. conditionOffset = 0;
  422. values.Add((value, caseBlockJump.Clone()));
  423. }
  424. if (values.Count != switchValueVarCopy.LoadCount)
  425. return false;
  426. context.Step(nameof(SimplifyCSharp1CascadingIfStatements), instructions[i]);
  427. // switch contains case null:
  428. if (currentCaseBlock != defaultOrNullBlock)
  429. {
  430. values.Add((null, new Branch(defaultOrNullBlock)));
  431. }
  432. var sections = new List<SwitchSection>(values.SelectWithIndex((index, b) => new SwitchSection { Labels = new LongSet(index), Body = b.Item2 }));
  433. sections.Add(new SwitchSection { Labels = new LongSet(new LongInterval(0, sections.Count)).Invert(), Body = new Branch(currentCaseBlock) });
  434. var stringToInt = new StringToInt(switchValue, values.SelectArray(item => item.Item1), context.TypeSystem.FindType(KnownTypeCode.String));
  435. var inst = new SwitchInstruction(stringToInt);
  436. inst.Sections.AddRange(sections);
  437. inst.AddILRange(instructions[i - 1]);
  438. instructions[i].ReplaceWith(inst);
  439. instructions.RemoveAt(i + 1);
  440. instructions.RemoveAt(i - 1);
  441. return true;
  442. }
  443. bool IsIsInternedCall(Call call, out ILInstruction argument)
  444. {
  445. if (call != null
  446. && call.Method.DeclaringType.IsKnownType(KnownTypeCode.String)
  447. && call.Method.IsStatic
  448. && call.Method.Name == "IsInterned"
  449. && call.Arguments.Count == 1)
  450. {
  451. argument = call.Arguments[0];
  452. return true;
  453. }
  454. argument = null;
  455. return false;
  456. }
  457. /// <summary>
  458. /// Each case consists of two blocks:
  459. /// 1. block:
  460. /// if (call op_Equality(ldloc switchVariable, ldstr value)) br caseBlock
  461. /// br nextBlock
  462. /// -or-
  463. /// if (comp(ldloc switchValueVar == ldnull)) br nextBlock
  464. /// br caseBlock
  465. /// 2. block is caseBlock
  466. /// This method matches the above pattern or its inverted form:
  467. /// the call to ==(string, string) is wrapped in logic.not and the branch targets are reversed.
  468. /// Returns the next block that follows in the block-chain.
  469. /// The <paramref name="switchVariable"/> is updated if the value gets copied to a different variable.
  470. /// See comments below for more info.
  471. /// </summary>
  472. ILInstruction MatchCaseBlock(Block currentBlock, ILVariable switchVariable, out string value, out bool emptyStringEqualsNull, out ILInstruction caseBlockOrLeave)
  473. {
  474. value = null;
  475. caseBlockOrLeave = null;
  476. emptyStringEqualsNull = false;
  477. if (currentBlock.IncomingEdgeCount != 1 || currentBlock.Instructions.Count != 2)
  478. return null;
  479. if (!currentBlock.MatchIfAtEndOfBlock(out var condition, out var caseBlockBranch, out var nextBlockBranch))
  480. return null;
  481. if (!MatchStringEqualityComparison(condition, switchVariable, out value, out bool isVBCompareString))
  482. {
  483. return null;
  484. }
  485. if (isVBCompareString)
  486. {
  487. ExtensionMethods.Swap(ref caseBlockBranch, ref nextBlockBranch);
  488. emptyStringEqualsNull = true;
  489. }
  490. if (caseBlockBranch.MatchBranch(out var caseBlock))
  491. {
  492. caseBlockOrLeave = caseBlock;
  493. }
  494. else if (caseBlockBranch.MatchLeave(out _))
  495. {
  496. caseBlockOrLeave = caseBlockBranch;
  497. }
  498. else
  499. {
  500. return null;
  501. }
  502. if (nextBlockBranch.MatchBranch(out Block nextBlock))
  503. {
  504. // success
  505. return nextBlock;
  506. }
  507. else if (nextBlockBranch.MatchLeave(out BlockContainer blockContainer))
  508. {
  509. // success
  510. return blockContainer;
  511. }
  512. else
  513. {
  514. return null;
  515. }
  516. }
  517. /// <summary>
  518. /// Matches the C# 2.0 switch-on-string pattern, which uses Dictionary&lt;string, int&gt;.
  519. /// </summary>
  520. bool MatchLegacySwitchOnStringWithDict(InstructionCollection<ILInstruction> instructions, ref int i)
  521. {
  522. // match first block: checking switch-value for null:
  523. // (In some cases, i.e., if switchValueVar is a parameter, the initial store is optional.)
  524. // stloc switchValueVar(switchValue)
  525. // if (comp(ldloc switchValueVar == ldnull)) br nullCase
  526. // br nextBlock
  527. if (!instructions[i].MatchIfInstruction(out var condition, out var exitBlockJump))
  528. return false;
  529. if (!(condition.MatchCompEquals(out var left, out var right) && right.MatchLdNull()))
  530. return false;
  531. // Extract switchValueVar
  532. if (!left.MatchLdLoc(out var switchValueVar) || !switchValueVar.IsSingleDefinition)
  533. return false;
  534. // If the switchValueVar is a stack slot and there is an assignment involving it right before the
  535. // switch-value null-check, we use the previously assigned variable as switchValueVar.
  536. ILInstruction switchValue;
  537. if (switchValueVar.Kind == VariableKind.StackSlot
  538. && instructions.ElementAtOrDefault(i - 1) is StLoc extraStore
  539. && extraStore.Value.MatchLdLoc(switchValueVar))
  540. {
  541. switchValueVar = extraStore.Variable;
  542. switchValue = extraStore.Value;
  543. }
  544. else
  545. {
  546. switchValue = null;
  547. }
  548. if (!switchValueVar.Type.IsKnownType(KnownTypeCode.String))
  549. return false;
  550. // either br nullCase or leave container
  551. BlockContainer leaveContainer = null;
  552. if (!exitBlockJump.MatchBranch(out var nullValueCaseBlock) && !exitBlockJump.MatchLeave(out leaveContainer))
  553. return false;
  554. var nextBlockJump = instructions.ElementAtOrDefault(i + 1) as Branch;
  555. if (nextBlockJump == null || nextBlockJump.TargetBlock.IncomingEdgeCount != 1)
  556. return false;
  557. // match second block: checking compiler-generated Dictionary<string, int> for null
  558. // if (comp(volatile.ldobj System.Collections.Generic.Dictionary`2[[System.String],[System.Int32]](ldsflda $$method0x600000c-1) != ldnull)) br caseNullBlock
  559. // br dictInitBlock
  560. var nextBlock = nextBlockJump.TargetBlock;
  561. if (nextBlock.Instructions.Count != 2 || !nextBlock.Instructions[0].MatchIfInstruction(out condition, out var tryGetValueBlockJump))
  562. return false;
  563. if (!tryGetValueBlockJump.MatchBranch(out var tryGetValueBlock))
  564. return false;
  565. if (!nextBlock.Instructions[1].MatchBranch(out var dictInitBlock) || dictInitBlock.IncomingEdgeCount != 1)
  566. return false;
  567. if (!(condition.MatchCompNotEquals(out left, out right) && right.MatchLdNull() &&
  568. MatchDictionaryFieldLoad(left, IsStringToIntDictionary, out var dictField, out var dictionaryType)))
  569. return false;
  570. // match third block: initialization of compiler-generated Dictionary<string, int>
  571. // stloc dict(newobj Dictionary..ctor(ldc.i4 valuesLength))
  572. // call Add(ldloc dict, ldstr value, ldc.i4 index)
  573. // ... more calls to Add ...
  574. // volatile.stobj System.Collections.Generic.Dictionary`2[[System.String],[System.Int32]](ldsflda $$method0x600003f-1, ldloc dict)
  575. // br switchHeadBlock
  576. if (dictInitBlock.IncomingEdgeCount != 1 || dictInitBlock.Instructions.Count < 3)
  577. return false;
  578. if (!ExtractStringValuesFromInitBlock(dictInitBlock, out var stringValues, out var blockAfterInit, dictionaryType, dictField, false))
  579. return false;
  580. if (tryGetValueBlock != blockAfterInit)
  581. return false;
  582. // match fourth block: TryGetValue on compiler-generated Dictionary<string, int>
  583. // if (logic.not(call TryGetValue(volatile.ldobj System.Collections.Generic.Dictionary`2[[System.String],[System.Int32]](ldsflda $$method0x600000c-1), ldloc switchValueVar, ldloca switchIndexVar))) br defaultBlock
  584. // br switchBlock
  585. if (tryGetValueBlock.IncomingEdgeCount != 2 || tryGetValueBlock.Instructions.Count != 2)
  586. return false;
  587. if (!tryGetValueBlock.Instructions[0].MatchIfInstruction(out condition, out var defaultBlockJump))
  588. return false;
  589. if (!defaultBlockJump.MatchBranch(out var defaultBlock) && !((leaveContainer != null && defaultBlockJump.MatchLeave(leaveContainer)) || defaultBlockJump.MatchLeave(out _)))
  590. return false;
  591. if (!(condition.MatchLogicNot(out var arg) && arg is CallInstruction c && c.Method.Name == "TryGetValue" &&
  592. MatchDictionaryFieldLoad(c.Arguments[0], IsStringToIntDictionary, out var dictField2, out _) && dictField2.Equals(dictField)))
  593. return false;
  594. if (!c.Arguments[1].MatchLdLoc(switchValueVar) || !c.Arguments[2].MatchLdLoca(out var switchIndexVar))
  595. return false;
  596. if (!tryGetValueBlock.Instructions[1].MatchBranch(out var switchBlock))
  597. return false;
  598. // match fifth block: switch-instruction block
  599. // switch (ldloc switchVariable) {
  600. // case [0..1): br caseBlock1
  601. // ... more cases ...
  602. // case [long.MinValue..0),[13..long.MaxValue]: br defaultBlock
  603. // }
  604. // mcs has a bug: when there is only one case it still generates the full-blown Dictionary<string, int> pattern,
  605. // but uses only a simple if statement instead of the switch instruction.
  606. if (switchBlock.IncomingEdgeCount != 1 || switchBlock.Instructions.Count == 0)
  607. return false;
  608. var sections = new List<SwitchSection>();
  609. switch (switchBlock.Instructions[0])
  610. {
  611. case SwitchInstruction switchInst:
  612. if (switchBlock.Instructions.Count != 1)
  613. return false;
  614. if (!switchInst.Value.MatchLdLoc(switchIndexVar))
  615. return false;
  616. sections.AddRange(switchInst.Sections);
  617. break;
  618. case IfInstruction ifInst:
  619. if (switchBlock.Instructions.Count != 2)
  620. return false;
  621. if (!ifInst.Condition.MatchCompEquals(out left, out right))
  622. return false;
  623. if (!left.MatchLdLoc(switchIndexVar))
  624. return false;
  625. if (!right.MatchLdcI4(0))
  626. return false;
  627. sections.Add(new SwitchSection() { Body = ifInst.TrueInst, Labels = new LongSet(0) }.WithILRange(ifInst));
  628. sections.Add(new SwitchSection() { Body = switchBlock.Instructions[1], Labels = new LongSet(0).Invert() }.WithILRange(switchBlock.Instructions[1]));
  629. break;
  630. }
  631. // mcs: map sections without a value to the default section, if possible
  632. if (!FixCasesWithoutValue(sections, stringValues))
  633. return false;
  634. // switch contains case null:
  635. if (nullValueCaseBlock != null && nullValueCaseBlock != defaultBlock)
  636. {
  637. if (!AddNullSection(sections, stringValues, nullValueCaseBlock))
  638. {
  639. return false;
  640. }
  641. }
  642. else if (leaveContainer != null && !defaultBlockJump.MatchLeave(leaveContainer))
  643. {
  644. if (!AddNullSection(sections, stringValues, (Leave)exitBlockJump))
  645. {
  646. return false;
  647. }
  648. }
  649. context.Step(nameof(MatchLegacySwitchOnStringWithDict), instructions[i]);
  650. bool keepAssignmentBefore = false;
  651. if (switchValueVar.LoadCount > 2 || switchValue == null)
  652. {
  653. switchValue = new LdLoc(switchValueVar);
  654. keepAssignmentBefore = true;
  655. }
  656. var stringToInt = new StringToInt(switchValue, stringValues, switchValueVar.Type);
  657. var inst = new SwitchInstruction(stringToInt);
  658. inst.Sections.AddRange(sections);
  659. instructions[i + 1].ReplaceWith(inst);
  660. if (keepAssignmentBefore)
  661. {
  662. // delete if (comp(ldloc switchValueVar == ldnull))
  663. inst.AddILRange(instructions[i]);
  664. instructions.RemoveAt(i);
  665. i--;
  666. }
  667. else
  668. {
  669. // delete both the if and the assignment before
  670. inst.AddILRange(instructions[i - 1]);
  671. instructions.RemoveRange(i - 1, 2);
  672. i -= 2;
  673. }
  674. return true;
  675. }
  676. bool FixCasesWithoutValue(List<SwitchSection> sections, List<(string, int)> stringValues)
  677. {
  678. bool HasLabel(SwitchSection section)
  679. {
  680. return section.Labels.Values.Any(i => stringValues.Any(value => i == value.Item2));
  681. }
  682. // Pick the section with the most labels as default section.
  683. // And collect all sections that have no value mapped to them.
  684. SwitchSection defaultSection = sections.First();
  685. List<SwitchSection> sectionsWithoutLabels = new List<SwitchSection>();
  686. foreach (var section in sections)
  687. {
  688. if (section == defaultSection)
  689. continue;
  690. if (section.Labels.Count() > defaultSection.Labels.Count())
  691. {
  692. if (!HasLabel(defaultSection))
  693. sectionsWithoutLabels.Add(defaultSection);
  694. defaultSection = section;
  695. continue;
  696. }
  697. if (!HasLabel(section))
  698. sectionsWithoutLabels.Add(section);
  699. }
  700. foreach (var section in sectionsWithoutLabels)
  701. {
  702. defaultSection.Labels = defaultSection.Labels.UnionWith(section.Labels);
  703. if (section.HasNullLabel)
  704. defaultSection.HasNullLabel = true;
  705. sections.Remove(section);
  706. }
  707. return true;
  708. }
  709. bool AddNullSection(List<SwitchSection> sections, List<(string Value, int Index)> stringValues, Block nullValueCaseBlock)
  710. {
  711. return AddNullSection(sections, stringValues, new Branch(nullValueCaseBlock));
  712. }
  713. bool AddNullSection(List<SwitchSection> sections, List<(string Value, int Index)> stringValues, ILInstruction body)
  714. {
  715. var label = new LongSet(stringValues.Max(item => item.Index) + 1);
  716. var possibleConflicts = sections.Where(sec => sec.Labels.Overlaps(label)).ToArray();
  717. if (possibleConflicts.Length > 1)
  718. return false;
  719. else if (possibleConflicts.Length == 1)
  720. {
  721. if (possibleConflicts[0].Labels.Count() == 1)
  722. return false; // cannot remove only label
  723. possibleConflicts[0].Labels = possibleConflicts[0].Labels.ExceptWith(label);
  724. }
  725. stringValues.Add((null, (int)label.Values.First()));
  726. sections.Add(new SwitchSection() { Labels = label, Body = body });
  727. return true;
  728. }
  729. /// <summary>
  730. /// Matches 'volatile.ldobj dictionaryType(ldsflda dictField)'
  731. /// </summary>
  732. bool MatchDictionaryFieldLoad(ILInstruction inst, Func<IType, bool> typeMatcher, out IField dictField, out IType dictionaryType)
  733. {
  734. dictField = null;
  735. dictionaryType = null;
  736. return inst.MatchLdObj(out var dictionaryFieldLoad, out dictionaryType) &&
  737. typeMatcher(dictionaryType) &&
  738. dictionaryFieldLoad.MatchLdsFlda(out dictField) &&
  739. (dictField.IsCompilerGeneratedOrIsInCompilerGeneratedClass() || dictField.Name.StartsWith("$$method", StringComparison.Ordinal));
  740. }
  741. /// <summary>
  742. /// Matches and extracts values from Add-call sequences.
  743. /// </summary>
  744. bool ExtractStringValuesFromInitBlock(Block block, out List<(string, int)> values, out Block blockAfterInit, IType dictionaryType, IField dictionaryField, bool isHashtablePattern)
  745. {
  746. values = null;
  747. blockAfterInit = null;
  748. // stloc dictVar(newobj Dictionary..ctor(ldc.i4 valuesLength))
  749. // -or-
  750. // stloc dictVar(newobj Hashtable..ctor(ldc.i4 capacity, ldc.f4 loadFactor))
  751. if (!(block.Instructions[0].MatchStLoc(out var dictVar, out var newObjDict) && newObjDict is NewObj newObj))
  752. return false;
  753. if (!newObj.Method.DeclaringType.Equals(dictionaryType))
  754. return false;
  755. int valuesLength = 0;
  756. if (newObj.Arguments.Count == 2)
  757. {
  758. if (!newObj.Arguments[0].MatchLdcI4(out valuesLength))
  759. return false;
  760. if (!newObj.Arguments[1].MatchLdcF4(0.5f))
  761. return false;
  762. }
  763. else if (newObj.Arguments.Count == 1)
  764. {
  765. if (!newObj.Arguments[0].MatchLdcI4(out valuesLength))
  766. return false;
  767. }
  768. values = new List<(string, int)>(valuesLength);
  769. int i = 0;
  770. while (MatchAddCall(dictionaryType, block.Instructions[i + 1], dictVar, out var index, out var value))
  771. {
  772. values.Add((value, index));
  773. i++;
  774. }
  775. // final store to compiler-generated variable:
  776. // volatile.stobj dictionaryType(ldsflda dictionaryField, ldloc dictVar)
  777. if (!(block.Instructions[i + 1].MatchStObj(out var loadField, out var dictVarLoad, out var dictType) &&
  778. dictType.Equals(dictionaryType) && loadField.MatchLdsFlda(out var dictField) && dictField.Equals(dictionaryField) &&
  779. dictVarLoad.MatchLdLoc(dictVar)))
  780. return false;
  781. if (isHashtablePattern && block.Instructions[i + 2] is IfInstruction)
  782. {
  783. return block.Instructions[i + 3].MatchBranch(out blockAfterInit);
  784. }
  785. return block.Instructions[i + 2].MatchBranch(out blockAfterInit);
  786. }
  787. /// <summary>
  788. /// call Add(ldloc dictVar, ldstr value, ldc.i4 index)
  789. /// -or-
  790. /// call Add(ldloc dictVar, ldstr value, box System.Int32(ldc.i4 index))
  791. /// </summary>
  792. bool MatchAddCall(IType dictionaryType, ILInstruction inst, ILVariable dictVar, out int index, out string value)
  793. {
  794. value = null;
  795. index = -1;
  796. if (!(inst is CallInstruction c && c.Method.Name == "Add" && c.Arguments.Count == 3))
  797. return false;
  798. if (!c.Arguments[0].MatchLdLoc(dictVar))
  799. return false;
  800. if (!c.Arguments[1].MatchLdStr(out value))
  801. {
  802. if (c.Arguments[1].MatchLdsFld(out var field) && field.DeclaringType.IsKnownType(KnownTypeCode.String) && field.Name == "Empty")
  803. {
  804. value = "";
  805. }
  806. else
  807. {
  808. return false;
  809. }
  810. }
  811. if (!(c.Method.DeclaringType.Equals(dictionaryType) && !c.Method.IsStatic))
  812. return false;
  813. return (c.Arguments[2].MatchLdcI4(out index) || (c.Arguments[2].MatchBox(out var arg, out _) && arg.MatchLdcI4(out index)));
  814. }
  815. bool IsStringToIntDictionary(IType dictionaryType)
  816. {
  817. if (dictionaryType.FullName != "System.Collections.Generic.Dictionary")
  818. return false;
  819. if (dictionaryType.TypeArguments.Count != 2)
  820. return false;
  821. return dictionaryType.TypeArguments[0].IsKnownType(KnownTypeCode.String) &&
  822. dictionaryType.TypeArguments[1].IsKnownType(KnownTypeCode.Int32);
  823. }
  824. bool IsNonGenericHashtable(IType dictionaryType)
  825. {
  826. if (dictionaryType.FullName != "System.Collections.Hashtable")
  827. return false;
  828. if (dictionaryType.TypeArguments.Count != 0)
  829. return false;
  830. return true;
  831. }
  832. bool MatchLegacySwitchOnStringWithHashtable(Block block, HashtableInitializer hashtableInitializers, ref int i)
  833. {
  834. // match first block: checking switch-value for null
  835. // stloc tmp(ldloc switch-value)
  836. // stloc switchVariable(ldloc tmp)
  837. // if (comp(ldloc tmp == ldnull)) br nullCaseBlock
  838. // br getItemBlock
  839. if (block.Instructions.Count != i + 4)
  840. return false;
  841. if (!block.Instructions[i].MatchStLoc(out var tmp, out var switchValue))
  842. return false;
  843. if (!block.Instructions[i + 1].MatchStLoc(out var switchVariable, out var tmpLoad) || !tmpLoad.MatchLdLoc(tmp))
  844. return false;
  845. if (!block.Instructions[i + 2].MatchIfInstruction(out var condition, out var nullCaseBlockBranch))
  846. return false;
  847. if (!block.Instructions[i + 3].MatchBranch(out var getItemBlock) || !(nullCaseBlockBranch.MatchBranch(out var nullCaseBlock) || nullCaseBlockBranch is Leave))
  848. return false;
  849. if (!(condition.MatchCompEquals(out var left, out var right) && right.MatchLdNull() && left.MatchLdLoc(tmp)))
  850. return false;
  851. // match second block: get_Item on compiler-generated Hashtable
  852. // stloc tmp2(call get_Item(volatile.ldobj System.Collections.Hashtable(ldsflda $$method0x600003f - 1), ldloc switchVariable))
  853. // stloc switchVariable(ldloc tmp2)
  854. // if (comp(ldloc tmp2 == ldnull)) br defaultCaseBlock
  855. // br switchBlock
  856. if (getItemBlock.IncomingEdgeCount != 1 || getItemBlock.Instructions.Count != 4)
  857. return false;
  858. if (!(getItemBlock.Instructions[0].MatchStLoc(out var tmp2, out var getItem) && getItem is Call getItemCall && getItemCall.Method.Name == "get_Item"))
  859. return false;
  860. if (!getItemBlock.Instructions[1].MatchStLoc(out var switchVariable2, out var tmp2Load) || !tmp2Load.MatchLdLoc(tmp2))
  861. return false;
  862. if (!ILVariableEqualityComparer.Instance.Equals(switchVariable, switchVariable2))
  863. return false;
  864. if (!getItemBlock.Instructions[2].MatchIfInstruction(out condition, out var defaultBlockBranch))
  865. return false;
  866. if (!getItemBlock.Instructions[3].MatchBranch(out var switchBlock) || !(defaultBlockBranch.MatchBranch(out var defaultBlock) || defaultBlockBranch is Leave))
  867. return false;
  868. if (!(condition.MatchCompEquals(out left, out right) && right.MatchLdNull() && left.MatchLdLoc(tmp2)))
  869. return false;
  870. if (!(getItemCall.Arguments.Count == 2 && MatchDictionaryFieldLoad(getItemCall.Arguments[0], IsNonGenericHashtable, out var dictField, out _) && getItemCall.Arguments[1].MatchLdLoc(switchVariable)))
  871. return false;
  872. // Check if there is a hashtable init block at the beginning of the method
  873. if (!hashtableInitializers.TryGetValue(dictField, out var info))
  874. return false;
  875. var stringValues = info.Labels;
  876. // match third block: switch-instruction block
  877. // switch (ldobj System.Int32(unbox System.Int32(ldloc switchVariable))) {
  878. // case [0..1): br caseBlock1
  879. // ... more cases ...
  880. // case [long.MinValue..0),[13..long.MaxValue]: br defaultBlock
  881. // }
  882. if (switchBlock.IncomingEdgeCount != 1 || switchBlock.Instructions.Count != 1)
  883. return false;
  884. if (!(switchBlock.Instructions[0] is SwitchInstruction switchInst && switchInst.Value.MatchLdObj(out var target, out var ldobjType) &&
  885. target.MatchUnbox(out var arg, out var unboxType) && arg.MatchLdLoc(switchVariable2) && ldobjType.IsKnownType(KnownTypeCode.Int32) && unboxType.Equals(ldobjType)))
  886. return false;
  887. var sections = new List<SwitchSection>(switchInst.Sections);
  888. // switch contains case null:
  889. if (!(nullCaseBlockBranch is Leave) && nullCaseBlock != defaultBlock)
  890. {
  891. if (!AddNullSection(sections, stringValues, nullCaseBlock))
  892. {
  893. return false;
  894. }
  895. }
  896. context.Step(nameof(MatchLegacySwitchOnStringWithHashtable), block.Instructions[i]);
  897. var stringToInt = new StringToInt(switchValue, stringValues, context.TypeSystem.FindType(KnownTypeCode.String));
  898. var inst = new SwitchInstruction(stringToInt);
  899. inst.Sections.AddRange(sections);
  900. inst.AddILRange(block.Instructions[i]);
  901. block.Instructions[i].ReplaceWith(inst);
  902. block.Instructions.RemoveRange(i + 1, 3);
  903. info.Transformed = true;
  904. hashtableInitializers[dictField] = info;
  905. return true;
  906. }
  907. bool FindHashtableInitBlock(Block entryPoint, out List<(string, int)> stringValues, out IField dictField, out Block blockAfterThisInitBlock, out ILInstruction thisSwitchInitJumpInst, out ILInstruction nextSwitchInitJumpInst)
  908. {
  909. stringValues = null;
  910. dictField = null;
  911. blockAfterThisInitBlock = null;
  912. nextSwitchInitJumpInst = null;
  913. thisSwitchInitJumpInst = null;
  914. if (entryPoint.Instructions.Count != 2)
  915. return false;
  916. // match first block: checking compiler-generated Hashtable for null
  917. // if (comp(volatile.ldobj System.Collections.Hashtable(ldsflda $$method0x600003f-1) != ldnull)) br switchHeadBlock
  918. // br tableInitBlock
  919. if (!(entryPoint.Instructions[0].MatchIfInstruction(out var condition, out var branchToSwitchHead)))
  920. return false;
  921. if (!entryPoint.Instructions[1].MatchBranch(out var tableInitBlock))
  922. return false;
  923. if (!(condition.MatchCompNotEquals(out var left, out var right) && right.MatchLdNull() &&
  924. MatchDictionaryFieldLoad(left, IsNonGenericHashtable, out dictField, out var dictionaryType)))
  925. return false;
  926. if (!branchToSwitchHead.MatchBranch(out var switchHead))
  927. return false;
  928. thisSwitchInitJumpInst = entryPoint.Instructions[0];
  929. // match second block: initialization of compiler-generated Hashtable
  930. // stloc table(newobj Hashtable..ctor(ldc.i4 capacity, ldc.f loadFactor))
  931. // call Add(ldloc table, ldstr value, box System.Int32(ldc.i4 index))
  932. // ... more calls to Add ...
  933. // volatile.stobj System.Collections.Hashtable(ldsflda $$method0x600003f - 1, ldloc table)
  934. // br switchHeadBlock
  935. if (tableInitBlock.IncomingEdgeCount != 1 || tableInitBlock.Instructions.Count < 3)
  936. return false;
  937. if (!ExtractStringValuesFromInitBlock(tableInitBlock, out stringValues, out blockAfterThisInitBlock, dictionaryType, dictField, true))
  938. return false;
  939. // if there is another IfInstruction before the end of the block, it might be a jump to the next hashtable init block.
  940. // if (comp(volatile.ldobj System.Collections.Hashtable(ldsflda $$method0x600003f-2) != ldnull)) br switchHeadBlock
  941. if (tableInitBlock.Instructions.SecondToLastOrDefault() is IfInstruction nextHashtableInitHead)
  942. {
  943. if (!(nextHashtableInitHead.Condition.MatchCompNotEquals(out left, out right) && right.MatchLdNull() &&
  944. MatchDictionaryFieldLoad(left, IsNonGenericHashtable, out var nextSwitchInitField, out _)))
  945. return false;
  946. if (!nextHashtableInitHead.TrueInst.MatchBranch(switchHead))
  947. return false;
  948. nextSwitchInitJumpInst = nextHashtableInitHead;
  949. }
  950. return true;
  951. }
  952. bool MatchRoslynSwitchOnString(InstructionCollection<ILInstruction> instructions, ref int i)
  953. {
  954. if (i >= instructions.Count - 1)
  955. return false;
  956. // stloc switchValueVar(switchValue)
  957. // if (comp(ldloc switchValueVar == ldnull)) br nullCase
  958. // br nextBlock
  959. InstructionCollection<ILInstruction> switchBlockInstructions = instructions;
  960. int switchBlockInstructionsOffset = i;
  961. Block nullValueCaseBlock = null;
  962. ILInstruction instForNullCheck = null;
  963. if (instructions[i].MatchIfInstruction(out var condition, out var exitBlockJump)
  964. && condition.MatchCompEqualsNull(out instForNullCheck))
  965. {
  966. var nextBlockJump = instructions[i + 1] as Branch;
  967. if (nextBlockJump == null || nextBlockJump.TargetBlock.IncomingEdgeCount != 1)
  968. return false;
  969. if (!exitBlockJump.MatchBranch(out nullValueCaseBlock))
  970. return false;
  971. switchBlockInstructions = nextBlockJump.TargetBlock.Instructions;
  972. switchBlockInstructionsOffset = 0;
  973. }
  974. // stloc switchValueVar(call ComputeStringHash(switchValueLoad))
  975. // switch (ldloc switchValueVar) {
  976. // case [211455823..211455824): br caseBlock1
  977. // ... more cases ...
  978. // case [long.MinValue..-365098645),...,[1697255802..long.MaxValue]: br defaultBlock
  979. // }
  980. if (!(switchBlockInstructionsOffset + 1 < switchBlockInstructions.Count
  981. && switchBlockInstructions[switchBlockInstructionsOffset + 1] is SwitchInstruction switchInst
  982. && switchInst.Value.MatchLdLoc(out var switchValueVar)
  983. && MatchComputeStringOrReadOnlySpanHashCall(switchBlockInstructions[switchBlockInstructionsOffset],
  984. switchValueVar, out LdLoc switchValueLoad)))
  985. {
  986. return false;
  987. }
  988. if (instForNullCheck != null && !instForNullCheck.MatchLdLoc(switchValueLoad.Variable))
  989. {
  990. return false;
  991. }
  992. var stringValues = new List<(string Value, ILInstruction TargetBlockOrLeave)>();
  993. SwitchSection defaultSection = switchInst.GetDefaultSection();
  994. if (!(defaultSection.Body.MatchBranch(out Block exitOrDefaultBlock) || defaultSection.Body.MatchLeave(out _)))
  995. return false;
  996. foreach (var section in switchInst.Sections)
  997. {
  998. if (section == defaultSection)
  999. continue;
  1000. // extract target block
  1001. if (!section.Body.MatchBranch(out Block target))
  1002. return false;
  1003. string stringValue;
  1004. bool emptyStringEqualsNull;
  1005. if (MatchRoslynEmptyStringCaseBlockHead(target, switchValueLoad.Variable, out ILInstruction targetOrLeave, out Block currentExitBlock))
  1006. {
  1007. stringValue = "";
  1008. emptyStringEqualsNull = false;
  1009. }
  1010. else if (!MatchRoslynCaseBlockHead(target, switchValueLoad.Variable, out targetOrLeave, out currentExitBlock, out stringValue, out emptyStringEqualsNull))
  1011. {
  1012. return false;
  1013. }
  1014. if (currentExitBlock != exitOrDefaultBlock)
  1015. return false;
  1016. if (emptyStringEqualsNull && string.IsNullOrEmpty(stringValue))
  1017. {
  1018. stringValues.Add((null, targetOrLeave));
  1019. stringValues.Add((string.Empty, targetOrLeave));
  1020. }
  1021. else
  1022. {
  1023. stringValues.Add((stringValue, targetOrLeave));
  1024. }
  1025. }
  1026. if (nullValueCaseBlock != null && exitOrDefaultBlock != nullValueCaseBlock)
  1027. {
  1028. stringValues.Add((null, nullValueCaseBlock));
  1029. }
  1030. // In newer Roslyn versions (>=3.7) the null check appears in the default case, not prior to the switch.
  1031. ILInstruction exitOrDefault = exitOrDefaultBlock;
  1032. if (!stringValues.Any(pair => pair.Value == null) && IsNullCheckInDefaultBlock(ref exitOrDefault, switchValueLoad.Variable, out nullValueCaseBlock))
  1033. {
  1034. stringValues.Add((null, nullValueCaseBlock));
  1035. }
  1036. exitOrDefaultBlock = (Block)exitOrDefault;
  1037. context.Step(nameof(MatchRoslynSwitchOnString), switchValueLoad);
  1038. if (exitOrDefaultBlock != null)
  1039. {
  1040. // change TargetBlock in case it was modified by IsNullCheckInDefaultBlock()
  1041. ((Branch)defaultSection.Body).TargetBlock = exitOrDefaultBlock;
  1042. }
  1043. ILInstruction switchValueInst = switchValueLoad;
  1044. if (instructions == switchBlockInstructions)
  1045. {
  1046. // stloc switchValueLoadVariable(switchValue)
  1047. // stloc switchValueVar(call ComputeStringHash(ldloc switchValueLoadVariable))
  1048. // switch (ldloc switchValueVar) {
  1049. bool keepAssignmentBefore;
  1050. // if the switchValueLoad.Variable is only used in the compiler generated case equality checks, we can remove it.
  1051. if (i >= 1 && instructions[i - 1].MatchStLoc(switchValueLoad.Variable, out var switchValueTmp) &&
  1052. switchValueLoad.Variable.IsSingleDefinition && switchValueLoad.Variable.LoadCount == switchInst.Sections.Count)
  1053. {
  1054. switchValueInst = switchValueTmp;
  1055. keepAssignmentBefore = false;
  1056. }
  1057. else
  1058. {
  1059. keepAssignmentBefore = true;
  1060. }
  1061. // replace stloc switchValueVar(call ComputeStringHash(...)) with new switch instruction
  1062. var newSwitch = ReplaceWithSwitchInstruction(i);
  1063. // remove old switch instruction
  1064. newSwitch.AddILRange(instructions[i + 1]);
  1065. instructions.RemoveAt(i + 1);
  1066. // remove extra assignment
  1067. if (!keepAssignmentBefore)
  1068. {
  1069. newSwitch.AddILRange(instructions[i - 1]);
  1070. instructions.RemoveRange(i - 1, 1);
  1071. i -= 1;
  1072. }
  1073. }
  1074. else
  1075. {
  1076. bool keepAssignmentBefore;
  1077. // if the switchValueLoad.Variable is only used in the compiler generated case equality checks, we can remove it.
  1078. if (i >= 2 && instructions[i - 2].MatchStLoc(out var temporary, out var temporaryValue) && instructions[i - 1].MatchStLoc(switchValueLoad.Variable, out var tempLoad) && tempLoad.MatchLdLoc(temporary))
  1079. {
  1080. switchValueInst = temporaryValue;
  1081. keepAssignmentBefore = false;
  1082. }
  1083. else
  1084. {
  1085. keepAssignmentBefore = true;
  1086. }
  1087. // replace null check with new switch instruction
  1088. var newSwitch = ReplaceWithSwitchInstruction(i);
  1089. newSwitch.AddILRange(switchInst);
  1090. // remove jump instruction to switch block
  1091. newSwitch.AddILRange(instructions[i + 1]);
  1092. instructions.RemoveAt(i + 1);
  1093. // remove extra assignment
  1094. if (!keepAssignmentBefore)
  1095. {
  1096. newSwitch.AddILRange(instructions[i - 2]);
  1097. instructions.RemoveRange(i - 2, 2);
  1098. i -= 2;
  1099. }
  1100. }
  1101. return true;
  1102. SwitchInstruction ReplaceWithSwitchInstruction(int offset)
  1103. {
  1104. var defaultLabel = new LongSet(new LongInterval(0, stringValues.Count)).Invert();
  1105. var values = new string[stringValues.Count];
  1106. var sections = new SwitchSection[stringValues.Count];
  1107. foreach (var (idx, (value, bodyInstruction)) in stringValues.WithIndex())
  1108. {
  1109. values[idx] = value;
  1110. var body = bodyInstruction is Block b ? new Branch(b) : bodyInstruction;
  1111. sections[idx] = new SwitchSection { Labels = new LongSet(idx), Body = body };
  1112. }
  1113. var newSwitch = new SwitchInstruction(new StringToInt(switchValueInst, values, switchValueLoad.Variable.Type));
  1114. newSwitch.Sections.AddRange(sections);
  1115. newSwitch.Sections.Add(new SwitchSection { Labels = defaultLabel, Body = defaultSection.Body });
  1116. instructions[offset].ReplaceWith(newSwitch);
  1117. return newSwitch;
  1118. }
  1119. }
  1120. private bool MatchRoslynSwitchOnStringUsingLengthAndChar(Block block, int i)
  1121. {
  1122. var instructions = block.Instructions;
  1123. // implements https://github.com/dotnet/roslyn/pull/66081
  1124. // if (comp(ldloc switchValueVar == ldnull)) br nullCase
  1125. // br nextBlock
  1126. Block switchOnLengthBlock;
  1127. int switchOnLengthBlockStartOffset;
  1128. Block nullCase = null;
  1129. if (instructions[i].MatchIfInstruction(out var condition, out var exitBlockJump)
  1130. && condition.MatchCompEqualsNull(out var ldloc)
  1131. && ldloc is LdLoc { Variable: var switchValueVar })
  1132. {
  1133. if (!instructions[i + 1].MatchBranch(out var nextBlock))
  1134. return false;
  1135. if (!exitBlockJump.MatchBranch(out nullCase) && !exitBlockJump.MatchLeave(out _))
  1136. return false;
  1137. // if (comp(ldloc switchValueVar == ldnull)) br ...
  1138. // br switchOnLengthBlock
  1139. if (nextBlock.IncomingEdgeCount == 1
  1140. && nextBlock.Instructions[0].MatchIfInstruction(out condition, out _)
  1141. && condition.MatchCompEqualsNull(out ldloc)
  1142. && ldloc.MatchLdLoc(switchValueVar))
  1143. {
  1144. if (!nextBlock.Instructions[1].MatchBranch(out switchOnLengthBlock))
  1145. return false;
  1146. }
  1147. else
  1148. {
  1149. switchOnLengthBlock = nextBlock;
  1150. }
  1151. if (switchOnLengthBlock.IncomingEdgeCount != 1)
  1152. return false;
  1153. switchOnLengthBlockStartOffset = 0;
  1154. }
  1155. else
  1156. {
  1157. switchOnLengthBlock = block;
  1158. switchValueVar = null; // will be extracted in MatchSwitchOnLengthBlock
  1159. switchOnLengthBlockStartOffset = i;
  1160. }
  1161. ILInstruction defaultCase = null;
  1162. if (!MatchSwitchOnLengthBlock(ref switchValueVar, switchOnLengthBlock, switchOnLengthBlockStartOffset, out var blocksByLength))
  1163. return false;
  1164. List<(string, ILInstruction)> stringValues = new();
  1165. foreach (var b in blocksByLength)
  1166. {
  1167. if (b.Length.Count() != 1)
  1168. {
  1169. if (b.TargetBlock != nullCase)
  1170. return false;
  1171. }
  1172. else
  1173. {
  1174. int length = (int)b.Length.Intervals[0].Start;
  1175. switch (b.TargetBlock)
  1176. {
  1177. case Leave leave:
  1178. break;
  1179. case Block targetBlock:
  1180. if (MatchSwitchOnCharBlock(targetBlock, length, switchValueVar, out var mapping))
  1181. {
  1182. foreach (var item in mapping)
  1183. {
  1184. if (!stringValues.Any(x => x.Item1 == item.StringValue))
  1185. {
  1186. stringValues.Add(item);
  1187. }
  1188. else
  1189. {
  1190. return false;
  1191. }
  1192. }
  1193. }
  1194. else if (MatchRoslynCaseBlockHead(targetBlock, switchValueVar, out var bodyOrLeave, out var exit, out string stringValue, out _))
  1195. {
  1196. if (exit != defaultCase)
  1197. return false;
  1198. if (!stringValues.Any(x => x.Item1 == stringValue))
  1199. {
  1200. stringValues.Add((stringValue, bodyOrLeave));
  1201. }
  1202. else
  1203. {
  1204. return false;
  1205. }
  1206. }
  1207. else if (length == 0)
  1208. {
  1209. stringValues.Add(("", b.TargetBlock));
  1210. }
  1211. else
  1212. {
  1213. return false;
  1214. }
  1215. break;
  1216. default:
  1217. return false;
  1218. }
  1219. }
  1220. }
  1221. if (!stringValues.Any(pair => pair.Item1 == null))
  1222. {
  1223. if (IsNullCheckInDefaultBlock(ref defaultCase, switchValueVar, out var nullBlock))
  1224. {
  1225. stringValues.Add((null, nullBlock));
  1226. }
  1227. else if (nullCase != null && nullCase != defaultCase)
  1228. {
  1229. stringValues.Add((null, nullCase));
  1230. }
  1231. }
  1232. context.Step(nameof(MatchRoslynSwitchOnStringUsingLengthAndChar), instructions[i]);
  1233. var defaultLabel = new LongSet(new LongInterval(0, stringValues.Count)).Invert();
  1234. var values = new string[stringValues.Count];
  1235. var sections = new SwitchSection[stringValues.Count];
  1236. foreach (var (idx, (value, bodyInstruction)) in stringValues.WithIndex())
  1237. {
  1238. values[idx] = value;
  1239. var body = bodyInstruction is Block b ? new Branch(b) : bodyInstruction;
  1240. sections[idx] = new SwitchSection { Labels = new LongSet(idx), Body = body };
  1241. }
  1242. var newSwitch = new SwitchInstruction(new StringToInt(new LdLoc(switchValueVar), values, switchValueVar.Type));
  1243. newSwitch.Sections.AddRange(sections);
  1244. newSwitch.Sections.Add(new SwitchSection { Labels = defaultLabel, Body = defaultCase is Block b2 ? new Branch(b2) : defaultCase });
  1245. newSwitch.AddILRange(instructions[i]);
  1246. if (nullCase != null)
  1247. {
  1248. newSwitch.AddILRange(instructions[i + 1]);
  1249. }
  1250. instructions[i] = newSwitch;
  1251. instructions.RemoveRange(i + 1, instructions.Count - (i + 1));
  1252. return true;
  1253. bool MatchGetChars(ILInstruction instruction, ILVariable switchValueVar, out int index)
  1254. {
  1255. index = -1;
  1256. if (context.Settings.SwitchOnReadOnlySpanChar && instruction.MatchLdObj(out var target, out var type) && type.IsKnownType(KnownTypeCode.UInt16))
  1257. {
  1258. return target is Call call
  1259. && (call.Method.FullNameIs("System.ReadOnlySpan", "get_Item")
  1260. || call.Method.FullNameIs("System.Span", "get_Item"))
  1261. && call.Arguments.Count == 2
  1262. && call.Arguments[0].MatchLdLoca(switchValueVar)
  1263. && call.Arguments[1].MatchLdcI4(out index);
  1264. }
  1265. else
  1266. {
  1267. return instruction is Call call
  1268. && call.Method.FullNameIs("System.String", "get_Chars")
  1269. && call.Arguments.Count == 2
  1270. && call.Arguments[0].MatchLdLoc(switchValueVar)
  1271. && call.Arguments[1].MatchLdcI4(out index);
  1272. }
  1273. }
  1274. bool MatchSwitchOnCharBlock(Block block, int length, ILVariable switchValueVar, out List<(string StringValue, ILInstruction BodyOrLeave)> results)
  1275. {
  1276. results = null;
  1277. if (block.IncomingEdgeCount != 1)
  1278. return false;
  1279. SwitchInstruction @switch;
  1280. List<KeyValuePair<LongSet, ILInstruction>> sections;
  1281. int index;
  1282. switch (block.Instructions.Count)
  1283. {
  1284. case 1:
  1285. @switch = block.Instructions[0] as SwitchInstruction;
  1286. if (@switch == null)
  1287. return false;
  1288. if (!MatchGetChars(@switch.Value, switchValueVar, out index))
  1289. return false;
  1290. sections = @switch.Sections.SelectList(s => new KeyValuePair<LongSet, ILInstruction>(s.Labels, s.Body));
  1291. break;
  1292. case 2:
  1293. if (!block.Instructions[0].MatchStLoc(out var charTempVar, out var getCharsCall))
  1294. return false;
  1295. if (!MatchGetChars(getCharsCall, switchValueVar, out index))
  1296. return false;
  1297. if (index < 0)
  1298. return false;
  1299. @switch = block.Instructions[1] as SwitchInstruction;
  1300. if (@switch == null)
  1301. return false;
  1302. if (!@switch.Value.MatchLdLoc(charTempVar))
  1303. return false;
  1304. sections = @switch.Sections.SelectList(s => new KeyValuePair<LongSet, ILInstruction>(s.Labels, s.Body));
  1305. break;
  1306. default:
  1307. if (!analysis.AnalyzeBlock(block))
  1308. {
  1309. return false;
  1310. }
  1311. if (!block.Instructions[0].MatchStLoc(out charTempVar, out getCharsCall))
  1312. return false;
  1313. if (!MatchGetChars(getCharsCall, switchValueVar, out index))
  1314. return false;
  1315. if (index < 0)
  1316. return false;
  1317. if (analysis.SwitchVariable != charTempVar)
  1318. return false;
  1319. sections = analysis.Sections;
  1320. break;
  1321. }
  1322. if (index >= length)
  1323. return false;
  1324. bool hasDefaultSection = false;
  1325. foreach (var (labels, body) in sections)
  1326. {
  1327. if (labels.Count() == 1)
  1328. {
  1329. char ch = unchecked((char)labels.Values.Single());
  1330. if (!body.MatchBranch(out var targetBlock))
  1331. return false;
  1332. if (length == 1)
  1333. {
  1334. results ??= new();
  1335. results.Add((ch.ToString(), targetBlock));
  1336. }
  1337. else
  1338. {
  1339. while (MatchRoslynCaseBlockHead(targetBlock, switchValueVar, out var bodyOrLeave, out var exit, out var stringValue, out _))
  1340. {
  1341. if (stringValue.Length != length || stringValue[index] != ch)
  1342. return false;
  1343. results ??= new();
  1344. results.Add((stringValue, bodyOrLeave));
  1345. if (exit == nullCase)
  1346. break;
  1347. targetBlock = exit;
  1348. }
  1349. }
  1350. }
  1351. else if (!hasDefaultSection)
  1352. {
  1353. hasDefaultSection = true;
  1354. }
  1355. else
  1356. {
  1357. return false;
  1358. }
  1359. }
  1360. return results?.Count > 0;
  1361. }
  1362. bool MatchSwitchOnLengthBlock(ref ILVariable switchValueVar, Block switchOnLengthBlock, int startOffset, out List<(LongSet Length, ILInstruction TargetBlock)> blocks)
  1363. {
  1364. blocks = null;
  1365. SwitchInstruction @switch;
  1366. ILInstruction getLengthCall;
  1367. ILVariable lengthVar;
  1368. switch (switchOnLengthBlock.Instructions.Count - startOffset)
  1369. {
  1370. case 1:
  1371. @switch = switchOnLengthBlock.Instructions[startOffset] as SwitchInstruction;
  1372. if (@switch == null)
  1373. return false;
  1374. getLengthCall = @switch.Value;
  1375. break;
  1376. case 2:
  1377. if (!switchOnLengthBlock.Instructions[startOffset].MatchStLoc(out lengthVar, out getLengthCall))
  1378. return false;
  1379. @switch = switchOnLengthBlock.Instructions[startOffset + 1] as SwitchInstruction;
  1380. if (@switch == null)
  1381. return false;
  1382. if (!@switch.Value.MatchLdLoc(lengthVar))
  1383. return false;
  1384. break;
  1385. case 3:
  1386. @switch = null;
  1387. if (!switchOnLengthBlock.Instructions[startOffset].MatchStLoc(out lengthVar, out getLengthCall))
  1388. return false;
  1389. if (!switchOnLengthBlock.Instructions[startOffset + 1].MatchIfInstruction(out var cond, out var gotoLength))
  1390. return false;
  1391. if (!gotoLength.MatchBranch(out var target))
  1392. return false;
  1393. if (!switchOnLengthBlock.Instructions[startOffset + 2].MatchBranch(out var gotoElse))
  1394. return false;
  1395. if (!cond.MatchCompEquals(out var lhs, out var rhs))
  1396. {
  1397. if (!cond.MatchCompNotEquals(out lhs, out rhs))
  1398. return false;
  1399. var t = target;
  1400. target = gotoElse;
  1401. gotoElse = t;
  1402. }
  1403. defaultCase = gotoElse;
  1404. if (!lhs.MatchLdLoc(lengthVar) || !rhs.MatchLdcI4(out int length))
  1405. return false;
  1406. blocks = new() {
  1407. (new LongSet(length), target),
  1408. (new LongSet(length).Invert(), defaultCase)
  1409. };
  1410. break;
  1411. default:
  1412. return false;
  1413. }
  1414. if (getLengthCall is not Call call
  1415. || call.Arguments.Count != 1
  1416. || call.Method.Name != "get_Length")
  1417. {
  1418. return false;
  1419. }
  1420. var declaringTypeCode = call.Method.DeclaringTypeDefinition?.KnownTypeCode;
  1421. switch (declaringTypeCode)
  1422. {
  1423. case KnownTypeCode.String:
  1424. if (!call.Arguments[0].MatchLdLoc(switchValueVar))
  1425. return false;
  1426. break;
  1427. case KnownTypeCode.ReadOnlySpanOfT:
  1428. case KnownTypeCode.SpanOfT:
  1429. if (!context.Settings.SwitchOnReadOnlySpanChar)
  1430. return false;
  1431. if (!call.Arguments[0].MatchLdLoca(out switchValueVar))
  1432. return false;
  1433. break;
  1434. default:
  1435. return false;
  1436. }
  1437. if (@switch == null)
  1438. return true;
  1439. blocks = new(@switch.Sections.Count);
  1440. foreach (var section in @switch.Sections)
  1441. {
  1442. if (section.HasNullLabel)
  1443. return false;
  1444. if (!section.Body.MatchBranch(out var target) && !section.Body.MatchLeave(out _))
  1445. return false;
  1446. ILInstruction targetInst = target ?? section.Body;
  1447. if (section.Labels.Count() != 1)
  1448. {
  1449. defaultCase ??= targetInst;
  1450. if (defaultCase != targetInst)
  1451. return false;
  1452. }
  1453. else
  1454. {
  1455. blocks.Add((section.Labels, targetInst));
  1456. }
  1457. }
  1458. return true;
  1459. }
  1460. }
  1461. /// <summary>
  1462. /// Matches:
  1463. /// Block oldDefaultBlock (incoming: 1) {
  1464. /// if (comp.o(ldloc switchVar == ldnull)) br nullValueCaseBlock
  1465. /// br newDefaultBlock
  1466. /// }
  1467. /// </summary>
  1468. private bool IsNullCheckInDefaultBlock(ref ILInstruction exitOrDefault, ILVariable switchVar, out Block nullValueCaseBlock)
  1469. {
  1470. nullValueCaseBlock = null;
  1471. if (exitOrDefault is not Block exitOrDefaultBlock)
  1472. return false;
  1473. if (!exitOrDefaultBlock.Instructions[0].MatchIfInstruction(out var condition, out var thenBranch))
  1474. return false;
  1475. if (!(condition.MatchCompEqualsNull(out var arg) && arg.MatchLdLoc(switchVar)))
  1476. return false;
  1477. if (!thenBranch.MatchBranch(out nullValueCaseBlock))
  1478. return false;
  1479. if (nullValueCaseBlock.Parent != exitOrDefaultBlock.Parent)
  1480. return false;
  1481. if (!exitOrDefaultBlock.Instructions[1].MatchBranch(out var elseBlock))
  1482. return false;
  1483. if (elseBlock.Parent != exitOrDefaultBlock.Parent)
  1484. return false;
  1485. exitOrDefault = elseBlock;
  1486. return true;
  1487. }
  1488. /// <summary>
  1489. /// Matches (and the negated version):
  1490. /// if (call op_Equality(ldloc switchValueVar, stringValue)) br body
  1491. /// br exit
  1492. /// </summary>
  1493. bool MatchRoslynCaseBlockHead(Block target, ILVariable switchValueVar, out ILInstruction bodyOrLeave, out Block defaultOrExitBlock, out string stringValue, out bool emptyStringEqualsNull)
  1494. {
  1495. bodyOrLeave = null;
  1496. defaultOrExitBlock = null;
  1497. stringValue = null;
  1498. emptyStringEqualsNull = false;
  1499. if (target.Instructions.Count != 2)
  1500. return false;
  1501. if (!target.Instructions[0].MatchIfInstruction(out var condition, out var bodyBranch))
  1502. {
  1503. // Special case: sometimes we don't have an if, because bodyBranch==exitBranch
  1504. // and the C# compiler optimized out the if.
  1505. // Example:
  1506. // Block IL_0063 (incoming: 1) {
  1507. // call op_Equality(ldloc V_4, ldstr "rowno")
  1508. // leave IL_0000(nop)
  1509. // }
  1510. condition = target.Instructions[0];
  1511. bodyBranch = target.Instructions[1];
  1512. }
  1513. ILInstruction exitBranch = target.Instructions[1];
  1514. // Handle negated conditions first:
  1515. while (condition.MatchLogicNot(out var expr))
  1516. {
  1517. ExtensionMethods.Swap(ref exitBranch, ref bodyBranch);
  1518. condition = expr;
  1519. }
  1520. if (!MatchStringEqualityComparison(condition, switchValueVar, out stringValue, out bool isVBCompareString))
  1521. return false;
  1522. if (isVBCompareString)
  1523. {
  1524. ExtensionMethods.Swap(ref exitBranch, ref bodyBranch);
  1525. emptyStringEqualsNull = true;
  1526. }
  1527. if (!(exitBranch.MatchBranch(out defaultOrExitBlock) || exitBranch.MatchLeave(out _)))
  1528. return false;
  1529. if (bodyBranch.MatchLeave(out _))
  1530. {
  1531. bodyOrLeave = bodyBranch;
  1532. return true;
  1533. }
  1534. if (bodyBranch.MatchBranch(out var bodyBlock))
  1535. {
  1536. bodyOrLeave = bodyBlock;
  1537. return true;
  1538. }
  1539. return false;
  1540. }
  1541. /// <summary>
  1542. /// Block target(incoming: 1) {
  1543. /// if (comp.o(ldloc switchValueVar == ldnull)) br exit
  1544. /// br lengthCheckBlock
  1545. /// }
  1546. ///
  1547. /// Block lengthCheckBlock(incoming: 1) {
  1548. /// if (logic.not(call get_Length(ldloc switchValueVar))) br body
  1549. /// br exit
  1550. /// }
  1551. /// </summary>
  1552. bool MatchRoslynEmptyStringCaseBlockHead(Block target, ILVariable switchValueVar, out ILInstruction bodyOrLeave, out Block defaultOrExitBlock)
  1553. {
  1554. bodyOrLeave = null;
  1555. defaultOrExitBlock = null;
  1556. if (target.Instructions.Count != 2 || target.IncomingEdgeCount != 1)
  1557. return false;
  1558. if (!target.Instructions[0].MatchIfInstruction(out var nullComparisonCondition, out var exitBranch))
  1559. return false;
  1560. if (!nullComparisonCondition.MatchCompEqualsNull(out var arg) || !arg.MatchLdLoc(switchValueVar))
  1561. return false;
  1562. if (!target.Instructions[1].MatchBranch(out Block lengthCheckBlock))
  1563. return false;
  1564. if (lengthCheckBlock.Instructions.Count != 2 || lengthCheckBlock.IncomingEdgeCount != 1)
  1565. return false;
  1566. if (!lengthCheckBlock.Instructions[0].MatchIfInstruction(out var lengthCheckCondition, out var exitBranch2))
  1567. return false;
  1568. ILInstruction bodyBranch;
  1569. if (lengthCheckCondition.MatchLogicNot(out arg))
  1570. {
  1571. bodyBranch = exitBranch2;
  1572. exitBranch2 = lengthCheckBlock.Instructions[1];
  1573. lengthCheckCondition = arg;
  1574. }
  1575. else
  1576. {
  1577. bodyBranch = lengthCheckBlock.Instructions[1];
  1578. }
  1579. if (!(exitBranch2.MatchBranch(out defaultOrExitBlock) || exitBranch2.MatchLeave(out _)))
  1580. return false;
  1581. if (!MatchStringLengthCall(lengthCheckCondition, switchValueVar))
  1582. return false;
  1583. if (!exitBranch.Match(exitBranch2).Success)
  1584. return false;
  1585. if (bodyBranch.MatchLeave(out _))
  1586. {
  1587. bodyOrLeave = bodyBranch;
  1588. return true;
  1589. }
  1590. if (bodyBranch.MatchBranch(out var bodyBlock))
  1591. {
  1592. bodyOrLeave = bodyBlock;
  1593. return true;
  1594. }
  1595. return false;
  1596. }
  1597. /// <summary>
  1598. /// call get_Length(ldloc switchValueVar)
  1599. /// </summary>
  1600. bool MatchStringLengthCall(ILInstruction inst, ILVariable switchValueVar)
  1601. {
  1602. return inst is Call call
  1603. && call.Method.DeclaringType.IsKnownType(KnownTypeCode.String)
  1604. && call.Method.IsAccessor
  1605. && call.Method.AccessorKind == System.Reflection.MethodSemanticsAttributes.Getter
  1606. && call.Method.AccessorOwner.Name == "Length"
  1607. && call.Arguments.Count == 1
  1608. && call.Arguments[0].MatchLdLoc(switchValueVar);
  1609. }
  1610. /// <summary>
  1611. /// Matches
  1612. /// 'stloc(targetVar, call ComputeStringHash(ldloc switchValue))'
  1613. /// - or -
  1614. /// 'stloc(targetVar, call ComputeSpanHash(ldloc switchValue))'
  1615. /// - or -
  1616. /// 'stloc(targetVar, call ComputeReadOnlySpanHash(ldloc switchValue))'
  1617. /// </summary>
  1618. internal static bool MatchComputeStringOrReadOnlySpanHashCall(ILInstruction inst, ILVariable targetVar, out LdLoc switchValue)
  1619. {
  1620. switchValue = null;
  1621. if (!inst.MatchStLoc(targetVar, out var value))
  1622. return false;
  1623. if (value is Call c && c.Arguments.Count == 1
  1624. && c.Method.Name is "ComputeStringHash" or "ComputeSpanHash" or "ComputeReadOnlySpanHash"
  1625. && c.Method.IsCompilerGeneratedOrIsInCompilerGeneratedClass())
  1626. {
  1627. if (c.Arguments[0] is not LdLoc ldloc)
  1628. return false;
  1629. switchValue = ldloc;
  1630. return true;
  1631. }
  1632. return false;
  1633. }
  1634. /// <summary>
  1635. /// Matches 'call string.op_Equality(ldloc(variable), ldstr stringValue)'
  1636. /// or 'comp(ldloc(variable) == ldnull)'
  1637. /// or 'call SequenceEqual(ldloc variable, call AsSpan(ldstr stringValue))'
  1638. /// </summary>
  1639. bool MatchStringEqualityComparison(ILInstruction condition, ILVariable variable, out string stringValue, out bool isVBCompareString)
  1640. {
  1641. return MatchStringEqualityComparison(condition, out var v, out stringValue, out isVBCompareString) && v == variable;
  1642. }
  1643. /// <summary>
  1644. /// Matches 'call string.op_Equality(ldloc(variable), ldstr stringValue)'
  1645. /// or 'comp(ldloc(variable) == ldnull)'
  1646. /// or 'call SequenceEqual(ldloc variable, call AsSpan(ldstr stringValue))'
  1647. /// </summary>
  1648. bool MatchStringEqualityComparison(ILInstruction condition, out ILVariable variable, out string stringValue, out bool isVBCompareString)
  1649. {
  1650. stringValue = null;
  1651. variable = null;
  1652. isVBCompareString = false;
  1653. while (condition is Comp comp && comp.Kind == ComparisonKind.Inequality && comp.Right.MatchLdcI4(0))
  1654. {
  1655. // if (x != 0) == if (x)
  1656. condition = comp.Left;
  1657. }
  1658. if (condition is Call c)
  1659. {
  1660. ILInstruction left, right;
  1661. if (c.Method.IsOperator && c.Method.Name == "op_Equality"
  1662. && c.Method.DeclaringType.IsKnownType(KnownTypeCode.String) && c.Arguments.Count == 2)
  1663. {
  1664. left = c.Arguments[0];
  1665. right = c.Arguments[1];
  1666. }
  1667. else if (c.Method.IsStatic && c.Method.Name == "CompareString"
  1668. && c.Method.DeclaringType.FullName == "Microsoft.VisualBasic.CompilerServices.Operators"
  1669. && c.Arguments.Count == 3)
  1670. {
  1671. left = c.Arguments[0];
  1672. right = c.Arguments[1];
  1673. // VB CompareString(): return 0 on equality -> condition is effectively negated.
  1674. // Also, the empty string is considered equal to null.
  1675. isVBCompareString = true;
  1676. if (!c.Arguments[2].MatchLdcI4(0))
  1677. {
  1678. // Option Compare Text: case insensitive comparison is not supported in C#
  1679. return false;
  1680. }
  1681. }
  1682. else if (c.Method.IsStatic && c.Method.Name == "SequenceEqual"
  1683. && c.Method.DeclaringType.FullName == "System.MemoryExtensions"
  1684. && c.Arguments.Count == 2)
  1685. {
  1686. left = c.Arguments[0];
  1687. if (c.Arguments[1] is Call {
  1688. Method.IsStatic: true,
  1689. Method.Name: "AsSpan",
  1690. Method.DeclaringType.FullName: "System.MemoryExtensions",
  1691. Arguments: [var ldStr]
  1692. } asSpanCall)
  1693. {
  1694. right = ldStr;
  1695. }
  1696. else
  1697. {
  1698. return false;
  1699. }
  1700. }
  1701. else
  1702. {
  1703. return false;
  1704. }
  1705. return left.MatchLdLoc(out variable) && right.MatchLdStr(out stringValue);
  1706. }
  1707. else if (condition.MatchCompEqualsNull(out var arg))
  1708. {
  1709. stringValue = null;
  1710. return arg.MatchLdLoc(out variable);
  1711. }
  1712. else
  1713. {
  1714. return false;
  1715. }
  1716. }
  1717. }
  1718. }