diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index a70de1e14..5accf84e3 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -224,6 +224,8 @@
+
+
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue3659.cs b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue3659.cs
new file mode 100644
index 000000000..9f5c35d72
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue3659.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Runtime.CompilerServices;
+using Microsoft.VisualBasic.CompilerServices;
+
+public class Issue3659
+{
+ public static void Func(ref Issue3659 obj, object value)
+ {
+ }
+
+ public static int ShowMessage(string a, int b, string c, object d, int e)
+ {
+ return 0;
+ }
+
+ internal void VBFunction(object value)
+ {
+#if OPT || LEGACY_VBC
+ int try0000_dispatch = -1;
+#else
+ int try0001_dispatch = -1;
+#endif
+ int num2 = default;
+ int num = default;
+ while (true)
+ {
+ try
+ {
+ /*Note: ILSpy has introduced the following switch to emulate a goto from catch-block to try-block*/;
+#if OPT || LEGACY_VBC
+ switch (try0000_dispatch)
+#else
+ switch (try0001_dispatch)
+#endif
+ {
+ default:
+ {
+ ProjectData.ClearProjectError();
+ num2 = 2;
+ Issue3659 obj = this;
+ Func(ref obj, RuntimeHelpers.GetObjectValue(value));
+#if OPT || LEGACY_VBC
+ goto end_IL_0000;
+#else
+ goto end_IL_0001;
+#endif
+ }
+#if OPT || LEGACY_VBC
+ case 45:
+#else
+ case 50:
+#endif
+ num = -1;
+ switch (num2)
+ {
+ case 2:
+ ShowMessage("VBFunction", 0, "Exception", null, 0);
+#if OPT || LEGACY_VBC
+ goto end_IL_0000;
+#else
+ goto end_IL_0001;
+#endif
+ }
+ break;
+ }
+ }
+ catch (Exception ex) when ((num2 != 0) & (num == 0))
+ {
+ ProjectData.SetProjectError(ex);
+#if OPT || LEGACY_VBC
+ try0000_dispatch = 45;
+#else
+ try0001_dispatch = 50;
+#endif
+ continue;
+ }
+ throw ProjectData.CreateProjectError(-2146828237);
+ continue;
+#if OPT || LEGACY_VBC
+ end_IL_0000:
+#else
+ end_IL_0001:
+#endif
+ break;
+ }
+ if (num != 0)
+ {
+ ProjectData.ClearProjectError();
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue3659.vb b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue3659.vb
new file mode 100644
index 000000000..1575fdd6d
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/VBPretty/Issue3659.vb
@@ -0,0 +1,17 @@
+Imports System
+Public Class Issue3659
+ Public Shared Sub Func(ByRef obj As Issue3659, value As Object)
+ End Sub
+
+ Public Shared Function ShowMessage(a As String, b As Integer, c As String, d As Object, e As Integer) As Integer
+ Return 0
+ End Function
+
+ Friend Sub VBFunction(value As Object)
+ On Error GoTo Handler
+ Func(Me, value)
+ Exit Sub
+Handler:
+ ShowMessage("VBFunction", 0, "Exception", Nothing, 0)
+ End Sub
+End Class
diff --git a/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs
index a700d4e7c..f608cca35 100644
--- a/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs
+++ b/ICSharpCode.Decompiler.Tests/VBPrettyTestRunner.cs
@@ -132,6 +132,12 @@ namespace ICSharpCode.Decompiler.Tests
await Run(options: options | CompilerOptions.Library);
}
+ [Test]
+ public async Task Issue3659([ValueSource(nameof(defaultOptions))] CompilerOptions options)
+ {
+ await Run(options: options | CompilerOptions.Library);
+ }
+
[Test]
public async Task Issue1906([ValueSource(nameof(defaultOptions))] CompilerOptions options)
{
diff --git a/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs b/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs
index 5aa9064e2..2cc1ba5ef 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/DetectCatchWhenConditionBlocks.cs
@@ -69,11 +69,79 @@ namespace ICSharpCode.Decompiler.IL.Transforms
container.SortBlocks(deleteUnreachableBlocks: true);
}
+ PropagateExceptionVariable(context, catchBlock);
+ }
+ else if (catchBlock.Filter is BlockContainer filterContainer
+ && MatchTypeTestConjunction(catchBlock.Variable, filterContainer,
+ out var conjunctionType, out var typeTest)
+ && conjunctionType.GetStackType() == catchBlock.Variable.StackType)
+ {
+ context.Step($"Detected catch-when type test for {catchBlock.Variable.Name}", typeTest);
+ catchBlock.Variable.Type = conjunctionType;
+ var remainder = typeTest.Right;
+ typeTest.ReplaceWith(remainder);
+ context.EndStep(remainder);
+
PropagateExceptionVariable(context, catchBlock);
}
}
}
+ ///
+ /// BlockContainer {
+ /// Block entryPoint (incoming: 1) {
+ /// leave container(bit.and(bit.and(comp(isinst T(ldloc exceptionVar) != ldnull), cond1), cond2))
+ /// }
+ /// }
+ /// The Visual Basic compiler emits the whole filter as one non-short-circuiting
+ /// expression rather than the block chain csc emits, so the type test sits in a
+ /// conjunction instead of a branch. On a match, typeTest is the innermost `bit.and`,
+ /// whose right operand is the filter that remains once the test moves to the catch type.
+ ///
+ bool MatchTypeTestConjunction(ILVariable exceptionVar, BlockContainer container,
+ out IType exceptionType, out BinaryNumericInstruction typeTest)
+ {
+ exceptionType = null;
+ typeTest = null;
+ var entryPoint = container.EntryPoint;
+ if (entryPoint == null || entryPoint.IncomingEdgeCount != 1 || entryPoint.Instructions.Count != 1)
+ return false;
+ if (!entryPoint.Instructions[0].MatchLeave(container, out var condition))
+ return false;
+ // Once the test moves to the catch type, the other conjuncts stop running for a
+ // non-matching exception, which is only invisible if they have no side effects.
+ if (!SemanticHelper.IsPure(condition.Flags))
+ return false;
+ // The test is the leftmost operand of a left-nested chain of `&`.
+ while (condition is BinaryNumericInstruction { Operator: BinaryNumericOperator.BitAnd } and)
+ {
+ if (MatchTypeTest(and.Left, exceptionVar, out exceptionType))
+ {
+ typeTest = and;
+ return true;
+ }
+ condition = and.Left;
+ }
+ return false;
+ }
+
+ ///
+ /// comp(isinst T(ldloc exceptionVar) != ldnull), however the compiler spelled the null test.
+ ///
+ static bool MatchTypeTest(ILInstruction condition, ILVariable exceptionVar, out IType exceptionType)
+ {
+ exceptionType = null;
+ if (condition is not Comp comp || !comp.Right.MatchLdNull())
+ return false;
+ // `cgt.un x, null` is how both compilers spell `x != null` for a reference.
+ if (comp.Kind != ComparisonKind.Inequality
+ && !(comp.Kind == ComparisonKind.GreaterThan && comp.InputType == StackType.O))
+ return false;
+ if (!comp.Left.MatchIsInst(out var argument, out exceptionType))
+ return false;
+ return argument.MatchLdLoc(exceptionVar);
+ }
+
///
/// catch E_189 : 0200007C System.Exception when (BlockContainer {
/// Block IL_0079 (incoming: 1) {