diff --git a/ICSharpCode.Decompiler.Tests/DebugStepRecordingTests.cs b/ICSharpCode.Decompiler.Tests/DebugStepRecordingTests.cs
index a6b3912b2..cbdda8c8c 100644
--- a/ICSharpCode.Decompiler.Tests/DebugStepRecordingTests.cs
+++ b/ICSharpCode.Decompiler.Tests/DebugStepRecordingTests.cs
@@ -43,6 +43,8 @@ namespace ICSharpCode.Decompiler.Tests
const string RecordedStep = "recorded IL step";
const string DetachedStep = "step on a detached function";
+ const string SeamStep = "C# AST built from ILAst";
+
static readonly FullTypeName SampleType =
new FullTypeName("ICSharpCode.Decompiler.CSharp.ProjectDecompiler.WholeProjectDecompiler");
@@ -212,6 +214,39 @@ namespace ICSharpCode.Decompiler.Tests
}
}
+ ///
+ /// The plain ExpressionBuilder/StatementBuilder output is a state worth looking at, so it has
+ /// a step of its own at the top level rather than being reachable only as "before the first
+ /// AST transform". Halting there has to print C#: the whole type is converted by then, so
+ /// nothing is left in ILAst.
+ ///
+ [Test]
+ public void TheStateAtTheSeamIsUntransformedCSharp()
+ {
+ if (!Stepper.SteppingAvailable)
+ Assert.Ignore("Transform stepping is compiled out without the STEP symbol, so there is no seam step.");
+
+ var decompiler = CreateRecordingDecompiler();
+ string full = decompiler.DecompileTypeAsString(SampleType);
+
+ var seam = decompiler.Stepper.Steps.SingleOrDefault(n => n.Description.Contains(SeamStep));
+ Assert.That(seam, Is.Not.Null, $"'{SeamStep}' must be recorded once, at the top level");
+
+ var replay = CreateRecordingDecompiler();
+ replay.Stepper.StepLimit = seam!.BeginStep;
+ string atSeam = replay.DecompileTypeAsString(SampleType);
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(replay.StepLimitHaltedFunction, Is.Null,
+ "the seam is past every member's IL phase, so there is no halted function to dump");
+ Assert.That(atSeam, Does.Contain("class WholeProjectDecompiler"),
+ "halting at the seam still prints C#");
+ Assert.That(atSeam, Is.Not.EqualTo(full),
+ "the AST transforms have not run yet, so this cannot equal the finished output");
+ }
+ }
+
///
/// A member group ends where the next member's group begins, so the state after its last step is
/// the state that member finished in - not the untouched IL of the member the halt unwinds from.
diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
index 70eea317f..ed0668734 100644
--- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
@@ -898,6 +898,11 @@ namespace ICSharpCode.Decompiler.CSharp
bool traceTransforms = DecompilerEventSource.Log.IsTransformTracingEnabled();
try
{
+ // The whole type has been converted and nothing has transformed it yet, so this is the
+ // one index whose state is the plain ExpressionBuilder/StatementBuilder output. Without
+ // it that state is only reachable as "before the first AST transform", which names a
+ // transform rather than the thing being shown and sits after every member's group.
+ context.Step("C# AST built from ILAst", rootNode);
foreach (var transform in astTransforms)
{
CancellationToken.ThrowIfCancellationRequested();