From 60f614f67a38df92b1da532a260fc223713c495f Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Tue, 20 Jun 2023 20:51:32 +0200 Subject: [PATCH 1/4] Roslyn 4.6 --- packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages.props b/packages.props index a347475be..4173951a7 100644 --- a/packages.props +++ b/packages.props @@ -27,10 +27,10 @@ 4.18.4 2017.7.26.1241 - 4.4.0 + 4.6.0 - 6.0.0 + 7.0.0 - 6.0.0 + 7.0.0 From 99d5e94a62dd657ce6dd52385d2ac5f839141307 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sat, 22 Jul 2023 22:05:39 +0200 Subject: [PATCH 2/4] Allow inlining value type temporaries into constrained call. --- .../TestCases/Correctness/ValueTypeCall.cs | 37 +++++++++++++++++++ ICSharpCode.Decompiler/DecompilerSettings.cs | 1 + .../IL/Transforms/ILInlining.cs | 17 +++++---- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ValueTypeCall.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ValueTypeCall.cs index e399012f9..45eaaceb7 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ValueTypeCall.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ValueTypeCall.cs @@ -16,6 +16,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness public void Dispose() { Console.WriteLine("MutValueType disposed on {0}", val); + val = val + 1; } public override string ToString() @@ -67,6 +68,10 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness gvt.Call(ref gvt); new ValueTypeCall().InstanceFieldTests(); ForEach(); +#if CS73 + DisposeMultipleTimes(ref m, in m); + ToStringGeneric(ref m, in m); +#endif } static void RefParameter(ref MutValueType m) @@ -213,5 +218,37 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness } Console.WriteLine("after: " + list[0].val); } + +#if CS73 + static void DisposeMultipleTimes(ref T mutRef, in T immutableRef) where T : struct, IDisposable + { + Console.WriteLine("DisposeMultipleTimes:"); + mutRef.Dispose(); + mutRef.Dispose(); + T copyFromMut = mutRef; + copyFromMut.Dispose(); + immutableRef.Dispose(); + immutableRef.Dispose(); + T copyFromImmutable = immutableRef; + copyFromImmutable.Dispose(); + mutRef.Dispose(); + immutableRef.Dispose(); + } + + static void ToStringGeneric(ref T mutRef, in T immutableRef) where T : struct + { + Console.WriteLine("ToStringGeneric:"); + mutRef.ToString(); + mutRef.ToString(); + T copyFromMut = mutRef; + copyFromMut.ToString(); + immutableRef.ToString(); + immutableRef.ToString(); + T copyFromImmutable = immutableRef; + copyFromImmutable.ToString(); + mutRef.ToString(); + immutableRef.ToString(); + } +#endif } } diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index dac595efa..b967cf067 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -378,6 +378,7 @@ namespace ICSharpCode.Decompiler } [Obsolete("Renamed to ScopedRef. This property will be removed in a future version of the decompiler.")] + [Browsable(false)] public bool LifetimeAnnotations { get { return ScopedRef; } set { ScopedRef = value; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index e357dfa44..fc15ebc3a 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -301,7 +301,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms // Thus, we have to ensure we're operating on an r-value. // Additionally, we cannot inline in cases where the C# compiler prohibits the direct use // of the rvalue (e.g. M(ref (MyStruct)obj); is invalid). - if (IsUsedAsThisPointerInCall(loadInst, out var method)) + if (IsUsedAsThisPointerInCall(loadInst, out var method, out var constrainedTo)) { if (options.HasFlag(InliningOptions.Aggressive)) { @@ -321,7 +321,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms case ExpressionClassification.ReadonlyLValue: // For struct method calls on readonly lvalues, the C# compiler // only generates a temporary if it isn't a "readonly struct" - return MethodRequiresCopyForReadonlyLValue(method); + return MethodRequiresCopyForReadonlyLValue(method, constrainedTo); default: throw new InvalidOperationException("invalid expression classification"); } @@ -337,11 +337,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } - internal static bool MethodRequiresCopyForReadonlyLValue(IMethod method) + internal static bool MethodRequiresCopyForReadonlyLValue(IMethod method, IType constrainedTo = null) { if (method == null) return true; - var type = method.DeclaringType; + var type = constrainedTo ?? method.DeclaringType; if (type.IsReferenceType == true) return false; // reference types are never implicitly copied if (method.ThisIsRefReadOnly) @@ -351,12 +351,13 @@ namespace ICSharpCode.Decompiler.IL.Transforms internal static bool IsUsedAsThisPointerInCall(LdLoca ldloca) { - return IsUsedAsThisPointerInCall(ldloca, out _); + return IsUsedAsThisPointerInCall(ldloca, out _, out _); } - static bool IsUsedAsThisPointerInCall(LdLoca ldloca, out IMethod method) + static bool IsUsedAsThisPointerInCall(LdLoca ldloca, out IMethod method, out IType constrainedType) { method = null; + constrainedType = null; if (ldloca.Variable.Type.IsReferenceType ?? false) return false; ILInstruction inst = ldloca; @@ -370,7 +371,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms { case OpCode.Call: case OpCode.CallVirt: - method = ((CallInstruction)inst.Parent).Method; + var callInst = (CallInstruction)inst.Parent; + method = callInst.Method; + constrainedType = callInst.ConstrainedTo; if (method.IsAccessor) { if (method.AccessorKind == MethodSemanticsAttributes.Getter) From cda7ddff48a4fae0bb1a043595891e86ff2c5bf2 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sat, 22 Jul 2023 22:19:12 +0200 Subject: [PATCH 3/4] Fix test case to actually check the ToString() results. --- .../TestCases/Correctness/ValueTypeCall.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ValueTypeCall.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ValueTypeCall.cs index 45eaaceb7..82b081632 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ValueTypeCall.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/ValueTypeCall.cs @@ -238,16 +238,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness static void ToStringGeneric(ref T mutRef, in T immutableRef) where T : struct { Console.WriteLine("ToStringGeneric:"); - mutRef.ToString(); - mutRef.ToString(); + Console.WriteLine(mutRef.ToString()); + Console.WriteLine(mutRef.ToString()); T copyFromMut = mutRef; - copyFromMut.ToString(); - immutableRef.ToString(); - immutableRef.ToString(); + Console.WriteLine(copyFromMut.ToString()); + Console.WriteLine(immutableRef.ToString()); + Console.WriteLine(immutableRef.ToString()); T copyFromImmutable = immutableRef; - copyFromImmutable.ToString(); - mutRef.ToString(); - immutableRef.ToString(); + Console.WriteLine(copyFromImmutable.ToString()); + Console.WriteLine(mutRef.ToString()); + Console.WriteLine(immutableRef.ToString()); } #endif } From 32e04eaf1298a7e4b3aa6ec510a9418039ee8b02 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 25 Jul 2023 19:04:22 +0200 Subject: [PATCH 4/4] Implement Roslyn 4.6 pattern for C# switch on string: match by length and unique characters first. --- .../IL/Transforms/SwitchOnStringTransform.cs | 336 +++++++++++++++++- 1 file changed, 335 insertions(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs index cefe2c261..58c9f169e 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs @@ -78,6 +78,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms changed = true; continue; } + if (MatchRoslynSwitchOnStringUsingLengthAndChar(block.Instructions, ref i)) + { + changed = true; + continue; + } } if (!changed) continue; @@ -1009,7 +1014,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms } var stringValues = new List<(string Value, ILInstruction TargetBlockOrLeave)>(); - SwitchSection defaultSection = switchInst.Sections.MaxBy(s => s.Labels.Count()); + SwitchSection defaultSection = switchInst.GetDefaultSection(); if (!(defaultSection.Body.MatchBranch(out Block exitOrDefaultBlock) || defaultSection.Body.MatchLeave(out _))) return false; foreach (var section in switchInst.Sections) @@ -1140,6 +1145,335 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + private bool MatchRoslynSwitchOnStringUsingLengthAndChar(InstructionCollection instructions, ref int i) + { + // implements https://github.com/dotnet/roslyn/pull/66081 + if (i >= instructions.Count - 1) + return false; + // if (comp(ldloc switchValueVar == ldnull)) br nullCase + // br nextBlock + if (!instructions[i].MatchIfInstruction(out var condition, out var exitBlockJump) + || !condition.MatchCompEqualsNull(out var ldloc) + || ldloc is not LdLoc { Variable: var switchValueVar }) + return false; + if (!instructions[i + 1].MatchBranch(out var defaultCase)) + return false; + if (!exitBlockJump.MatchBranch(out var nullCase)) + return false; + // if (comp(ldloc switchValueVar == ldnull)) br defaultCase + // br switchOnLengthBlock + Block switchOnLengthBlock; + if (defaultCase.IncomingEdgeCount == 1 + && defaultCase.Instructions[0].MatchIfInstruction(out condition, out exitBlockJump) + && condition.MatchCompEqualsNull(out ldloc) + && ldloc.MatchLdLoc(switchValueVar)) + { + if (!defaultCase.Instructions[1].MatchBranch(out switchOnLengthBlock)) + return false; + if (!exitBlockJump.MatchBranch(out defaultCase)) + return false; + } + else + { + switchOnLengthBlock = defaultCase; + defaultCase = nullCase; + } + if (!MatchSwitchOnLengthBlock(switchValueVar, switchOnLengthBlock, out var blocksByLength)) + return false; + List<(string, ILInstruction)> stringValues = new(); + foreach (var block in blocksByLength) + { + if (block.Length.Count() != 1) + { + if (block.TargetBlock != nullCase) + return false; + } + else + { + int length = (int)block.Length.Intervals[0].Start; + if (MatchSwitchOnCharBlock(block.TargetBlock, length, switchValueVar, out var mapping) + || MatchIfElseOnCharBlock(block.TargetBlock, length, switchValueVar, out mapping)) + { + foreach (var item in mapping) + { + if (!stringValues.Any(x => x.Item1 == item.StringValue)) + { + stringValues.Add(item); + } + else + { + return false; + } + } + } + else if (MatchRoslynCaseBlockHead(block.TargetBlock, switchValueVar, out var bodyOrLeave, out var exit, out string stringValue, out _)) + { + if (exit != nullCase) + return false; + if (!stringValues.Any(x => x.Item1 == stringValue)) + { + stringValues.Add((stringValue, bodyOrLeave)); + } + else + { + return false; + } + } + else if (length == 0) + { + stringValues.Add(("", block.TargetBlock)); + } + else + { + return false; + } + } + } + + if (!stringValues.Any(pair => pair.Item1 == null)) + { + if (IsNullCheckInDefaultBlock(ref defaultCase, switchValueVar, out var nullBlock)) + { + stringValues.Add((null, nullBlock)); + } + else if (nullCase != defaultCase) + { + stringValues.Add((null, nullCase)); + } + } + + context.Step(nameof(MatchRoslynSwitchOnStringUsingLengthAndChar), instructions[i]); + var defaultLabel = new LongSet(new LongInterval(0, stringValues.Count)).Invert(); + var values = new string[stringValues.Count]; + var sections = new SwitchSection[stringValues.Count]; + foreach (var (idx, (value, bodyInstruction)) in stringValues.WithIndex()) + { + values[idx] = value; + var body = bodyInstruction is Block b ? new Branch(b) : bodyInstruction; + sections[idx] = new SwitchSection { Labels = new LongSet(idx), Body = body }; + } + var newSwitch = new SwitchInstruction(new StringToInt(new LdLoc(switchValueVar), values)); + newSwitch.Sections.AddRange(sections); + newSwitch.Sections.Add(new SwitchSection { Labels = defaultLabel, Body = new Branch(defaultCase) }); + newSwitch.AddILRange(instructions[i]); + newSwitch.AddILRange(instructions[i + 1]); + instructions[i].ReplaceWith(newSwitch); + instructions.RemoveAt(i + 1); + return true; + + bool MatchGetChars(ILInstruction instruction, ILVariable switchValueVar, out int index) + { + index = -1; + return instruction is Call call + && call.Method.FullNameIs("System.String", "get_Chars") + && call.Arguments.Count == 2 + && call.Arguments[0].MatchLdLoc(switchValueVar) + && call.Arguments[1].MatchLdcI4(out index); + } + + bool MatchSwitchOnCharBlock(Block block, int length, ILVariable switchValueVar, out List<(string StringValue, ILInstruction BodyOrLeave)> results) + { + results = null; + if (block.IncomingEdgeCount != 1) + return false; + SwitchInstruction @switch; + int index; + switch (block.Instructions.Count) + { + case 1: + @switch = block.Instructions[0] as SwitchInstruction; + if (@switch == null) + return false; + if (!MatchGetChars(@switch.Value, switchValueVar, out index)) + return false; + break; + case 2: + if (!block.Instructions[0].MatchStLoc(out var charTempVar, out var getCharsCall)) + return false; + if (!MatchGetChars(getCharsCall, switchValueVar, out index)) + return false; + if (index < 0) + return false; + @switch = block.Instructions[1] as SwitchInstruction; + if (@switch == null) + return false; + if (!@switch.Value.MatchLdLoc(charTempVar)) + return false; + break; + default: + return false; + } + if (index >= length) + return false; + SwitchSection defaultSection = null; + foreach (var section in @switch.Sections) + { + if (section.Labels.Count() == 1) + { + char ch = unchecked((char)section.Labels.Values.Single()); + if (!section.Body.MatchBranch(out var targetBlock)) + return false; + if (length == 1) + { + results ??= new(); + results.Add((ch.ToString(), targetBlock)); + } + else + { + while (MatchRoslynCaseBlockHead(targetBlock, switchValueVar, out var bodyOrLeave, out var exit, out var stringValue, out _)) + { + if (stringValue.Length != length || stringValue[index] != ch) + return false; + results ??= new(); + results.Add((stringValue, bodyOrLeave)); + if (exit == nullCase) + break; + targetBlock = exit; + } + } + } + else if (defaultSection == null) + { + defaultSection = section; + } + else + { + return false; + } + } + return results?.Count > 0; + } + + bool MatchIfElseOnCharBlock(Block startOfChainBlock, int length, ILVariable switchValueVar, out List<(string StringValue, ILInstruction BodyOrLeave)> results) + { + results = null; + if (startOfChainBlock.IncomingEdgeCount != 1) + return false; + if (startOfChainBlock.Instructions.Count != 3) + return false; + if (!startOfChainBlock.Instructions[0].MatchStLoc(out var charTempVar, out var getCharsCall)) + return false; + if (!MatchGetChars(getCharsCall, switchValueVar, out int index)) + return false; + if (index < 0) + return false; + if (index >= length) + return false; + var currentBlock = startOfChainBlock; + int offset = 1; + while (true) + { + if (!currentBlock.Instructions[offset].MatchIfInstruction(out var condition, out var gotoHead)) + break; + if (!condition.MatchCompEquals(out var left, out var right)) + break; + if (!left.MatchLdLoc(charTempVar) || !right.MatchLdcI4(out int i)) + break; + if (!currentBlock.Instructions[offset + 1].MatchBranch(out var nextBlock)) + break; + if (!gotoHead.MatchBranch(out var headBlock)) + break; + if (!MatchRoslynCaseBlockHead(headBlock, switchValueVar, out var bodyOrLeave, out var exit, out var stringValue, out _)) + break; + if (exit != nullCase) + return false; + results ??= new(); + results.Add((stringValue, bodyOrLeave)); + offset = 0; + currentBlock = nextBlock; + } + return true; + } + + bool MatchSwitchOnLengthBlock(ILVariable switchValueVar, Block switchOnLengthBlock, out List<(LongSet Length, Block TargetBlock)> blocks) + { + blocks = null; + if (switchOnLengthBlock.IncomingEdgeCount != 1) + return false; + SwitchInstruction @switch; + ILInstruction getLengthCall; + ILVariable lengthVar; + if (switchOnLengthBlock.FinalInstruction is not Nop) + return false; + switch (switchOnLengthBlock.Instructions.Count) + { + case 1: + @switch = switchOnLengthBlock.Instructions[0] as SwitchInstruction; + if (@switch == null) + return false; + getLengthCall = @switch.Value; + break; + case 2: + if (!switchOnLengthBlock.Instructions[0].MatchStLoc(out lengthVar, out getLengthCall)) + return false; + @switch = switchOnLengthBlock.Instructions[1] as SwitchInstruction; + if (@switch == null) + return false; + if (!@switch.Value.MatchLdLoc(lengthVar)) + return false; + break; + case 3: + @switch = null; + if (!switchOnLengthBlock.Instructions[0].MatchStLoc(out lengthVar, out getLengthCall)) + return false; + if (!switchOnLengthBlock.Instructions[1].MatchIfInstruction(out var cond, out var gotoLength)) + return false; + if (!gotoLength.MatchBranch(out var target)) + return false; + if (!switchOnLengthBlock.Instructions[2].MatchBranch(out var gotoElse)) + return false; + if (!cond.MatchCompEquals(out var lhs, out var rhs)) + { + if (!cond.MatchCompNotEquals(out lhs, out rhs)) + return false; + var t = target; + target = gotoElse; + gotoElse = t; + } + if (gotoElse != defaultCase) + return false; + if (!lhs.MatchLdLoc(lengthVar) || !rhs.MatchLdcI4(out int length)) + return false; + + blocks = new() { + (new LongSet(length), target), + (new LongSet(length).Invert(), defaultCase) + }; + break; + default: + return false; + } + if (getLengthCall is not Call call + || call.Arguments.Count != 1 + || !call.Method.FullNameIs("System.String", "get_Length")) + { + return false; + } + if (!call.Arguments[0].MatchLdLoc(switchValueVar)) + return false; + if (@switch == null) + return true; + blocks = new(@switch.Sections.Count); + foreach (var section in @switch.Sections) + { + if (section.HasNullLabel) + return false; + if (!section.Body.MatchBranch(out var target)) + return false; + if (section.Labels.Count() != 1) + { + if (!section.Body.MatchBranch(defaultCase)) + return false; + } + else + { + blocks.Add((section.Labels, target)); + } + } + return true; + } + } + /// /// Matches: /// Block oldDefaultBlock (incoming: 1) {