From b8d68d91430da6d9edc335b49817c52b9eca5121 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Thu, 27 Aug 2026 09:24:09 +0200 Subject: [PATCH] Give the builder output a step of its own The state the ExpressionBuilder and StatementBuilder leave behind was only reachable as "state before the first AST transform" - an entry that names a transform rather than the state, and that sits below every member's group in a tree with one group per member. It is now a top-level step at the seam, where the whole type is converted and nothing has transformed it yet. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../DebugStepRecordingTests.cs | 35 +++++++++++++++++++ .../CSharp/CSharpDecompiler.cs | 5 +++ 2 files changed, 40 insertions(+) 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();