mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
The pane used to split the pipeline across two languages: the ILAst language stepped the IL transforms, the C# language stepped the AST transforms, and nothing showed the seam between them, so a step index meant a different thing depending on which language happened to be selected. Recording both halves into one Stepper makes an index replayable across the whole pipeline; a limit that lands in the IL phase has no C# to print, so the halted function is rendered as ILAst instead. Which function that is takes some care, because a member group's EndStep is the next member's first step: a halt standing on a member's opening step belongs to the member that just finished, a transform that throws where the limit was aimed has to hand over the ILAst it half-transformed (what the ILAst language showed as "ILAst after the crash"), and a step recorded on a helper function the pipeline has not attached yet belongs to that function's own tree. Retention stays opt-in twice over: the decompiler records IL steps only when asked to, and the pane asks only while its view is on screen. Every kept step pins the ILAst it captured, which for one type runs to tens of thousands of nodes, so a closed pane would be paying for a tree nobody displays. What is left of the ILAst language is its typed-IL dump, which runs no transforms at all. That stays, as TypedILLanguage. IDebugStepProvider was down to a single implementation and is removed. Assisted-by: Claude:claude-opus-5[1m]:Claude Codepull/4029/head
17 changed files with 976 additions and 445 deletions
@ -0,0 +1,311 @@
@@ -0,0 +1,311 @@
|
||||
// Copyright (c) 2026 Siegfried Pammer
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
|
||||
using ICSharpCode.Decompiler.CSharp; |
||||
using ICSharpCode.Decompiler.DebugSteps; |
||||
using ICSharpCode.Decompiler.IL; |
||||
using ICSharpCode.Decompiler.IL.Transforms; |
||||
using ICSharpCode.Decompiler.Metadata; |
||||
using ICSharpCode.Decompiler.Tests.Helpers; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
using NUnit.Framework; |
||||
|
||||
namespace ICSharpCode.Decompiler.Tests |
||||
{ |
||||
/// <summary>
|
||||
/// The Debug Steps view replays a decompilation by index, so a single <see cref="Stepper"/> has to
|
||||
/// span the whole pipeline: the IL transforms, the ILAst-to-C# conversion, and the C# AST
|
||||
/// transforms. These tests pin the IL half of that recording.
|
||||
/// </summary>
|
||||
[TestFixture] |
||||
public class DebugStepRecordingTests |
||||
{ |
||||
const string RecordedStep = "recorded IL step"; |
||||
const string DetachedStep = "step on a detached function"; |
||||
|
||||
static readonly FullTypeName SampleType = |
||||
new FullTypeName("ICSharpCode.Decompiler.CSharp.ProjectDecompiler.WholeProjectDecompiler"); |
||||
|
||||
[Test] |
||||
public void ILTransformStepsLandOnTheDecompilerStepper() |
||||
{ |
||||
var decompiler = CreateRecordingDecompiler(); |
||||
decompiler.ILTransforms.Add(new RecordingILTransform()); |
||||
|
||||
decompiler.DecompileTypeAsString(SampleType); |
||||
|
||||
Assert.That(Descriptions(decompiler.Stepper.Steps), Has.Some.Contains(RecordedStep)); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Retaining the IL steps of every member of a type is affordable for a step view and not for a
|
||||
/// whole-module decompile, which is why it is opt-in - and why callers that never opted in must
|
||||
/// keep the throwaway per-member stepper they have always had.
|
||||
/// </summary>
|
||||
[Test] |
||||
public void ILTransformStepsAreNotRecordedWithoutOptIn() |
||||
{ |
||||
var decompiler = StepperTesting.CreateDecompiler(); |
||||
decompiler.ILTransforms.Add(new RecordingILTransform()); |
||||
|
||||
decompiler.DecompileTypeAsString(SampleType); |
||||
|
||||
Assert.That(Descriptions(decompiler.Stepper.Steps), Has.None.Contains(RecordedStep)); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Halting in the IL phase leaves no C# to show, so the caller needs the ILAst the pipeline
|
||||
/// stopped in - and must not be told the member failed to decompile.
|
||||
/// </summary>
|
||||
[Test] |
||||
public void AStepLimitInTheILPhaseHaltsWithThePartiallyTransformedFunction() |
||||
{ |
||||
var decompiler = CreateRecordingDecompiler(); |
||||
decompiler.ILTransforms.Insert(0, new RecordingILTransform()); |
||||
decompiler.Stepper.StepLimit = 0; |
||||
|
||||
string code = decompiler.DecompileTypeAsString(SampleType); |
||||
|
||||
var astTransformNames = CSharpDecompiler.GetAstTransforms().Select(t => t.GetType().Name).ToArray(); |
||||
using (Assert.EnterMultipleScope()) |
||||
{ |
||||
Assert.That(decompiler.StepLimitHaltedFunction, Is.Not.Null, "the caller renders this as ILAst"); |
||||
Assert.That(decompiler.Stepper.LimitReachedStep, Is.Not.Null); |
||||
Assert.That(decompiler.Errors, Is.Empty, "a deliberate halt is not a decompilation failure"); |
||||
Assert.That(code, Does.Not.Contain(CSharpDecompiler.DecompilationErrorReportUrl)); |
||||
// The C# AST phase must not run after an IL-phase halt. It would hit the same limit
|
||||
// again - the step counter never advances past it - and the second hit would replace
|
||||
// LimitReachedStep with an AST node, so the position the halted step is highlighted at
|
||||
// would be lost.
|
||||
Assert.That(astTransformNames, Has.None.Matches<string>( |
||||
name => decompiler.Stepper.LimitReachedStep!.Description.Contains(name)), |
||||
"the halt must still report the IL step it stopped on"); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// A limit in the C# AST phase prints the partially transformed tree, and nothing claims the
|
||||
/// run halted in the IL phase.
|
||||
/// </summary>
|
||||
[Test] |
||||
public void AStepLimitInTheAstPhaseStillProducesPartialCSharp() |
||||
{ |
||||
var decompiler = StepperTesting.CreateDecompiler(); |
||||
decompiler.Stepper.StepLimit = 1; |
||||
|
||||
string code = decompiler.DecompileTypeAsString(SampleType); |
||||
|
||||
using (Assert.EnterMultipleScope()) |
||||
{ |
||||
Assert.That(decompiler.StepLimitHaltedFunction, Is.Null); |
||||
Assert.That(code, Does.Contain("DecompileProject"), "the members are still written"); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// A member whose transform throws must not swallow the members after it: their steps belong
|
||||
/// beside it in the tree, not inside the group it abandoned.
|
||||
/// </summary>
|
||||
[Test] |
||||
public void AFailingTransformDoesNotNestLaterMembersUnderIt() |
||||
{ |
||||
if (!Stepper.SteppingAvailable) |
||||
Assert.Ignore("Transform stepping is compiled out without the STEP symbol, so there are no groups to check."); |
||||
|
||||
var decompiler = CreateRecordingDecompiler(); |
||||
decompiler.ILTransforms.Add(new StepperTesting.ThrowingILTransform("CleanUpFileName")); |
||||
|
||||
decompiler.DecompileTypeAsString(SampleType); |
||||
|
||||
var crashed = decompiler.Stepper.Steps.Single(n => n.Description.Contains("CleanUpFileName")); |
||||
using (Assert.EnterMultipleScope()) |
||||
{ |
||||
Assert.That(crashed.EndStep, Is.GreaterThan(crashed.BeginStep + 1), "the abandoned group was closed at the step it stopped on"); |
||||
Assert.That(decompiler.Stepper.Steps, Has.Some.Matches<Stepper.Node>(n => n.Description.Contains("SanitizeFileName")), |
||||
"the members after the failing one stay top-level siblings"); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Closing the groups an unwind abandoned must stop at the depth the member started from: a
|
||||
/// group opened around the decompile still belongs to whoever opened it, and swallowing it
|
||||
/// files every later step as that caller's sibling instead of its child.
|
||||
/// </summary>
|
||||
[Test] |
||||
public void AFailingTransformLeavesAGroupOpenedAroundItAlone() |
||||
{ |
||||
if (!Stepper.SteppingAvailable) |
||||
Assert.Ignore("Transform stepping is compiled out without the STEP symbol, so there are no groups to check."); |
||||
|
||||
var decompiler = CreateRecordingDecompiler(); |
||||
decompiler.ILTransforms.Add(new StepperTesting.ThrowingILTransform("CleanUpFileName")); |
||||
|
||||
var outer = decompiler.Stepper.StartGroup("outer"); |
||||
decompiler.DecompileTypeAsString(SampleType); |
||||
var afterwards = decompiler.Stepper.Step("after the decompile"); |
||||
|
||||
using (Assert.EnterMultipleScope()) |
||||
{ |
||||
Assert.That(outer.Children, Does.Contain(afterwards), |
||||
"the group opened around the decompile must still be the one collecting steps"); |
||||
Assert.That(decompiler.Stepper.Steps, Does.Not.Contain(afterwards), |
||||
"a step recorded inside the outer group is not a top-level sibling of it"); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Test] |
||||
public void TheStateAfterAMemberGroupIsThatMembersTransformedFunction() |
||||
{ |
||||
if (!Stepper.SteppingAvailable) |
||||
Assert.Ignore("Transform stepping is compiled out without the STEP symbol, so there are no groups to replay."); |
||||
|
||||
int endStep = MemberGroup("CleanUpFileName", CreateRecordingDecompiler()).EndStep; |
||||
|
||||
var replay = CreateRecordingDecompiler(); |
||||
replay.Stepper.StepLimit = endStep; |
||||
replay.DecompileTypeAsString(SampleType); |
||||
|
||||
Assert.That(replay.StepLimitHaltedFunction?.Name, Is.EqualTo("CleanUpFileName")); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Replaying the state after a group whose transform threw stops on the exception, never on a
|
||||
/// step: the ILAst the crash left half-transformed is the whole point of looking at it.
|
||||
/// </summary>
|
||||
[Test] |
||||
public void TheStateAfterACrashedGroupIsTheFunctionTheTransformCrashedIn() |
||||
{ |
||||
if (!Stepper.SteppingAvailable) |
||||
Assert.Ignore("Transform stepping is compiled out without the STEP symbol, so there are no groups to replay."); |
||||
|
||||
var decompiler = CreateRecordingDecompiler(); |
||||
decompiler.ILTransforms.Add(new StepperTesting.ThrowingILTransform("CleanUpFileName")); |
||||
decompiler.DecompileTypeAsString(SampleType); |
||||
int endStep = decompiler.Stepper.Steps.Single(n => n.Description.Contains("CleanUpFileName")).EndStep; |
||||
|
||||
var replay = CreateRecordingDecompiler(); |
||||
replay.ILTransforms.Add(new StepperTesting.ThrowingILTransform("CleanUpFileName")); |
||||
replay.Stepper.StepLimit = endStep; |
||||
replay.DecompileTypeAsString(SampleType); |
||||
|
||||
Assert.That(replay.StepLimitHaltedFunction?.Name, Is.EqualTo("CleanUpFileName")); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Some transforms build a helper function and work on it before attaching it to the member
|
||||
/// (ProxyCallReplacer, the nested-function decompilers). A halt in there has to render the tree
|
||||
/// that holds the halted instruction; the member's function does not contain it yet.
|
||||
/// </summary>
|
||||
[Test] |
||||
public void AHaltOnADetachedFunctionRendersThatFunction() |
||||
{ |
||||
var detachedStep = new DetachedFunctionILTransform("CleanUpFileName"); |
||||
var decompiler = CreateRecordingDecompiler(); |
||||
decompiler.ILTransforms.Add(detachedStep); |
||||
decompiler.DecompileTypeAsString(SampleType); |
||||
int haltAt = AllNodes(decompiler.Stepper.Steps).Single(n => n.Description.Contains(DetachedStep)).BeginStep; |
||||
|
||||
var replay = CreateRecordingDecompiler(); |
||||
var replayStep = new DetachedFunctionILTransform("CleanUpFileName"); |
||||
replay.ILTransforms.Add(replayStep); |
||||
replay.Stepper.StepLimit = haltAt; |
||||
replay.DecompileTypeAsString(SampleType); |
||||
|
||||
Assert.That(replay.StepLimitHaltedFunction, Is.SameAs(replayStep.DetachedFunction)); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Runs the decompiler once and returns the top-level group recording the named member's IL phase.
|
||||
/// </summary>
|
||||
static Stepper.Node MemberGroup(string methodName, CSharpDecompiler decompiler) |
||||
{ |
||||
decompiler.DecompileTypeAsString(SampleType); |
||||
return decompiler.Stepper.Steps.Single(n => n.Description.EndsWith("." + methodName, StringComparison.Ordinal)); |
||||
} |
||||
|
||||
static IEnumerable<Stepper.Node> AllNodes(IEnumerable<Stepper.Node> nodes) |
||||
{ |
||||
foreach (var node in nodes) |
||||
{ |
||||
yield return node; |
||||
foreach (var child in AllNodes(node.Children)) |
||||
yield return child; |
||||
} |
||||
} |
||||
|
||||
static IEnumerable<string> Descriptions(IEnumerable<Stepper.Node> nodes) |
||||
{ |
||||
foreach (var node in nodes) |
||||
{ |
||||
yield return node.Description; |
||||
foreach (var description in Descriptions(node.Children)) |
||||
yield return description; |
||||
} |
||||
} |
||||
|
||||
static CSharpDecompiler CreateRecordingDecompiler() |
||||
{ |
||||
var decompiler = StepperTesting.CreateDecompiler(); |
||||
decompiler.RecordILTransformSteps = true; |
||||
return decompiler; |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Records one step per top-level function through <see cref="Stepper.Step"/> rather than
|
||||
/// <c>context.Step</c>: the latter is <c>[Conditional("STEP")]</c>, and that is resolved where
|
||||
/// the call is compiled - this assembly, which never defines STEP - so a <c>context.Step</c>
|
||||
/// call would vanish and leave the tests passing vacuously in every configuration.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Records one step on an instruction of a function that hangs off nothing, standing in for the
|
||||
/// helper functions the real transforms build before attaching them.
|
||||
/// </summary>
|
||||
sealed class DetachedFunctionILTransform(string methodName) : IILTransform |
||||
{ |
||||
public ILFunction? DetachedFunction { get; private set; } |
||||
|
||||
public void Run(ILFunction function, ILTransformContext context) |
||||
{ |
||||
if (function.Parent != null || function.Method?.Name != methodName) |
||||
return; |
||||
DetachedFunction = new ILFunction(function.Method!, function.CodeSize, function.GenericContext, |
||||
new Nop(), ILFunctionKind.LocalFunction); |
||||
context.Stepper.Step(DetachedStep, new DebugStepNodeInfo(DetachedFunction.Body)); |
||||
} |
||||
} |
||||
|
||||
sealed class RecordingILTransform : IILTransform |
||||
{ |
||||
public void Run(ILFunction function, ILTransformContext context) |
||||
{ |
||||
if (function.Parent == null) |
||||
context.Stepper.Step(RecordedStep); |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2026 Siegfried Pammer
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System; |
||||
|
||||
using ICSharpCode.Decompiler.CSharp; |
||||
using ICSharpCode.Decompiler.IL; |
||||
using ICSharpCode.Decompiler.IL.Transforms; |
||||
using ICSharpCode.Decompiler.Metadata; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
|
||||
namespace ICSharpCode.Decompiler.Tests.Helpers |
||||
{ |
||||
/// <summary>
|
||||
/// Shared setup for the tests that drive the transform pipeline itself rather than the code it
|
||||
/// produces: they decompile this assembly, which is on disk next to the tests and large enough to
|
||||
/// span many members, and they need a transform that fails on demand.
|
||||
/// </summary>
|
||||
static class StepperTesting |
||||
{ |
||||
public const string SimulatedFailure = "Simulated transform failure"; |
||||
|
||||
public static CSharpDecompiler CreateDecompiler() |
||||
{ |
||||
var module = new PEFile("ICSharpCode.Decompiler.dll"); |
||||
var settings = new DecompilerSettings(); |
||||
var typeSystem = new DecompilerTypeSystem(module, new UniversalAssemblyResolver(null, false, null), settings); |
||||
return new CSharpDecompiler(typeSystem, settings); |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Throws while transforming the named method's top-level function, leaving the pipeline to
|
||||
/// unwind out of whatever step groups it had opened.
|
||||
/// </summary>
|
||||
public sealed class ThrowingILTransform(string methodName) : IILTransform |
||||
{ |
||||
public void Run(ILFunction function, ILTransformContext context) |
||||
{ |
||||
if (function.Parent == null && function.Method?.Name == methodName) |
||||
throw new InvalidOperationException(SimulatedFailure); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -1,52 +0,0 @@
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#if DEBUG
|
||||
|
||||
using System; |
||||
|
||||
using ICSharpCode.Decompiler.DebugSteps; |
||||
|
||||
namespace ICSharpCode.ILSpy.Languages |
||||
{ |
||||
/// <summary>
|
||||
/// A language that surfaces a <see cref="Stepper"/> of decompiler-pipeline steps for the
|
||||
/// Debug Steps pane to visualise. Implemented by <see cref="ILAstLanguage"/> (one node per IL
|
||||
/// transform step) and <see cref="CSharpLanguage"/> (one node per C# AST transform). The pane
|
||||
/// binds to whichever current language is an <see cref="IDebugStepProvider"/>, so it no longer has to
|
||||
/// know the concrete language type.
|
||||
/// </summary>
|
||||
internal interface IDebugStepProvider |
||||
{ |
||||
/// <summary>The step tree produced by the most recent full decompile.</summary>
|
||||
Stepper Stepper { get; } |
||||
|
||||
/// <summary>Raised after a full (non step-limited) decompile refreshes <see cref="Stepper"/>.</summary>
|
||||
event EventHandler? StepperUpdated; |
||||
|
||||
/// <summary>
|
||||
/// Language-specific options object the Debug Steps pane renders above the step tree
|
||||
/// (e.g. ILAst's writing-options checkboxes), or <see langword="null"/> when the language
|
||||
/// has no step options. The pane picks a template by the object's runtime type, so each
|
||||
/// language contributes its own controls.
|
||||
/// </summary>
|
||||
object? StepOptions { get; } |
||||
} |
||||
} |
||||
|
||||
#endif
|
||||
@ -1,201 +0,0 @@
@@ -1,201 +0,0 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#if DEBUG
|
||||
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Composition; |
||||
|
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.Decompiler.CSharp; |
||||
using ICSharpCode.Decompiler.DebugSteps; |
||||
using ICSharpCode.Decompiler.Disassembler; |
||||
using ICSharpCode.Decompiler.IL; |
||||
using ICSharpCode.Decompiler.IL.Transforms; |
||||
using ICSharpCode.Decompiler.Metadata; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX; |
||||
|
||||
using ICSharpCode.ILSpy.AppEnv; |
||||
using ICSharpCode.ILSpy.Docking; |
||||
using ICSharpCode.ILSpy.TextView; |
||||
using ICSharpCode.ILSpy.ViewModels; |
||||
|
||||
using SRM = System.Reflection.Metadata; |
||||
|
||||
namespace ICSharpCode.ILSpy.Languages |
||||
{ |
||||
/// <summary>
|
||||
/// Debug-only language that surfaces the decompiler pipeline's intermediate state.
|
||||
/// Two concrete variants ship: <see cref="TypedIL"/> renders raw IL with type
|
||||
/// annotations; <see cref="BlockIL"/> runs the decompiler's IL transforms with a
|
||||
/// <see cref="Stepper"/> attached so the Debug Steps pane can replay each transform.
|
||||
/// Compiled only when DEBUG is defined — the language list is identical to Release
|
||||
/// otherwise.
|
||||
/// </summary>
|
||||
public abstract class ILAstLanguage : Language, IDebugStepProvider |
||||
{ |
||||
readonly string name; |
||||
|
||||
protected ILAstLanguage(string name) |
||||
{ |
||||
this.name = name; |
||||
} |
||||
|
||||
// ILAst output uses the same `{}/()/[]` bracket conventions as C#, plus C#-style
|
||||
// `//` comments and `"..."` strings. Reusing CSharpBracketSearcher gives the
|
||||
// language correct bracket highlighting without a per-grammar implementation.
|
||||
public override ICSharpCode.ILSpy.TextView.IBracketSearcher BracketSearcher { get; } = new CSharpBracketSearcher(); |
||||
|
||||
/// <summary>
|
||||
/// Fires after a <see cref="DecompileMethod"/> run installs a fresh <see cref="Stepper"/>.
|
||||
/// The Debug Steps pane subscribes here so it can rebind its TreeView to the new
|
||||
/// step list whenever the user reruns decompilation.
|
||||
/// </summary>
|
||||
public event EventHandler? StepperUpdated; |
||||
|
||||
protected virtual void OnStepperUpdated(EventArgs? e = null) |
||||
=> StepperUpdated?.Invoke(this, e ?? EventArgs.Empty); |
||||
|
||||
public Stepper Stepper { get; set; } = new(); |
||||
|
||||
// ILAst contributes the shared writing-options checkboxes to the Debug Steps pane.
|
||||
public object? StepOptions => DebugStepsPaneModel.WritingOptions; |
||||
|
||||
public override string Name => name; |
||||
|
||||
public override string FileExtension => ".il"; |
||||
|
||||
public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options) |
||||
{ |
||||
base.DecompileMethod(method, output, options); |
||||
new ReflectionDisassembler(output, options.CancellationToken) |
||||
.DisassembleMethodHeader(method.ParentModule!.MetadataFile, (SRM.MethodDefinitionHandle)method.MetadataToken); |
||||
output.WriteLine(); |
||||
output.WriteLine(); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Raw IL with type annotations on each instruction. No transforms, no stepper — the
|
||||
/// Debug Steps pane stays empty under this language because there's nothing to step
|
||||
/// through. Useful as a sanity-check view for the IL reader itself.
|
||||
/// </summary>
|
||||
[Export(typeof(Language))] |
||||
[Shared] |
||||
public sealed class TypedILLanguage : ILAstLanguage |
||||
{ |
||||
public TypedILLanguage() : base("Typed IL") { } |
||||
|
||||
public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options) |
||||
{ |
||||
base.DecompileMethod(method, output, options); |
||||
var module = method.ParentModule!.MetadataFile!; |
||||
var methodDef = module.Metadata.GetMethodDefinition((SRM.MethodDefinitionHandle)method.MetadataToken); |
||||
if (!methodDef.HasBody()) |
||||
return; |
||||
var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver()); |
||||
var reader = new ILReader(typeSystem.MainModule); |
||||
var methodBody = module.GetMethodBody(methodDef.RelativeVirtualAddress); |
||||
reader.WriteTypedIL((SRM.MethodDefinitionHandle)method.MetadataToken, methodBody, output, cancellationToken: options.CancellationToken); |
||||
} |
||||
} |
||||
|
||||
/// <summary>
|
||||
/// Runs the full C# decompiler's IL transforms and writes the resulting <see cref="ILFunction"/>.
|
||||
/// Each transform is recorded by a <see cref="Stepper"/> hooked into the
|
||||
/// <see cref="ILTransformContext"/>; the resulting step tree is what the Debug Steps
|
||||
/// pane visualises. The "Show Steps" button on the output reveals the pane.
|
||||
/// </summary>
|
||||
[Export(typeof(Language))] |
||||
[Shared] |
||||
public sealed class BlockILLanguage : ILAstLanguage |
||||
{ |
||||
readonly IReadOnlyList<IILTransform> transforms; |
||||
|
||||
public BlockILLanguage() : base("ILAst") |
||||
{ |
||||
this.transforms = CSharpDecompiler.GetILTransforms(); |
||||
} |
||||
|
||||
public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options) |
||||
{ |
||||
base.DecompileMethod(method, output, options); |
||||
var module = method.ParentModule!.MetadataFile!; |
||||
var metadata = module.Metadata; |
||||
var methodDef = metadata.GetMethodDefinition((SRM.MethodDefinitionHandle)method.MetadataToken); |
||||
if (!methodDef.HasBody()) |
||||
return; |
||||
IAssemblyResolver assemblyResolver = module.GetAssemblyResolver(); |
||||
var typeSystem = new DecompilerTypeSystem(module, assemblyResolver); |
||||
var reader = new ILReader(typeSystem.MainModule) { |
||||
UseDebugSymbols = options.DecompilerSettings.UseDebugSymbols, |
||||
UseRefLocalsForAccurateOrderOfEvaluation = options.DecompilerSettings.UseRefLocalsForAccurateOrderOfEvaluation, |
||||
}; |
||||
var methodBody = module.GetMethodBody(methodDef.RelativeVirtualAddress); |
||||
ILFunction il = reader.ReadIL((SRM.MethodDefinitionHandle)method.MetadataToken, methodBody, |
||||
kind: ILFunctionKind.TopLevelFunction, cancellationToken: options.CancellationToken); |
||||
var decompiler = new CSharpDecompiler(typeSystem, options.DecompilerSettings) { CancellationToken = options.CancellationToken }; |
||||
ILTransformContext context = decompiler.CreateILTransformContext(il); |
||||
context.Stepper.StepLimit = options.StepLimit; |
||||
context.Stepper.IsDebug = options.IsDebug; |
||||
try |
||||
{ |
||||
il.RunTransforms(transforms, context); |
||||
} |
||||
catch (StepLimitReachedException) |
||||
{ |
||||
// Expected when the Debug Steps pane asked to halt after a specific step.
|
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
output.WriteLine(ex.ToString()); |
||||
output.WriteLine(); |
||||
output.WriteLine("ILAst after the crash:"); |
||||
} |
||||
finally |
||||
{ |
||||
// Capture the populated stepper even when a transform crashed, so the user
|
||||
// can see the partial step tree leading up to the failure point. Only when
|
||||
// the run was full-fidelity (no StepLimit) — partial runs leave the stepper
|
||||
// alone so the previously-shown tree stays visible.
|
||||
if (options.StepLimit == int.MaxValue) |
||||
{ |
||||
Stepper = context.Stepper; |
||||
OnStepperUpdated(); |
||||
} |
||||
} |
||||
// DockWorkspace is resolved lazily here, not via [ImportingConstructor]: it imports
|
||||
// LanguageService, which imports the registered Languages, so a constructor import would
|
||||
// form a composition cycle. The lazy lookup costs one MEF resolve per "Show Steps" click.
|
||||
(output as ISmartTextOutput)?.AddButton(Images.ViewCode, "Show Steps", delegate { |
||||
AppComposition.TryGetExport<DockWorkspace>()?.ShowToolPane(DebugStepsPaneModel.PaneContentId); |
||||
}); |
||||
output.WriteLine(); |
||||
il.WriteTo(output, DebugStepsPaneModel.WritingOptions); |
||||
if (output is TextView.AvaloniaEditTextOutput nodeOutput |
||||
&& TextView.DebugStepHighlighter.TryResolve(context.Stepper, options.StepLimit, options.HighlightStep, nodeOutput.NodeLookup, out var range)) |
||||
{ |
||||
nodeOutput.DebugStepHighlight = range; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
#endif
|
||||
@ -0,0 +1,72 @@
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||
// software and associated documentation files (the "Software"), to deal in the Software
|
||||
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||
// to whom the Software is furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all copies or
|
||||
// substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#if DEBUG
|
||||
|
||||
using System.Composition; |
||||
|
||||
using ICSharpCode.Decompiler; |
||||
using ICSharpCode.Decompiler.Disassembler; |
||||
using ICSharpCode.Decompiler.IL; |
||||
using ICSharpCode.Decompiler.TypeSystem; |
||||
using ICSharpCode.ILSpyX; |
||||
|
||||
using SRM = System.Reflection.Metadata; |
||||
|
||||
namespace ICSharpCode.ILSpy.Languages |
||||
{ |
||||
/// <summary>
|
||||
/// Debug-only language rendering raw IL with the type the reader inferred for each instruction.
|
||||
/// It runs no transforms, so it is a sanity-check view for the IL reader itself rather than a
|
||||
/// stage of the decompiler pipeline - the pipeline is walked in the Debug Steps pane instead,
|
||||
/// under the C# language.
|
||||
/// Compiled only when DEBUG is defined; the language list is identical to Release otherwise.
|
||||
/// </summary>
|
||||
[Export(typeof(Language))] |
||||
[Shared] |
||||
public sealed class TypedILLanguage : Language |
||||
{ |
||||
public override string Name => "Typed IL"; |
||||
|
||||
public override string FileExtension => ".il"; |
||||
|
||||
// Typed IL output uses the same `{}/()/[]` bracket conventions as C#, plus C#-style
|
||||
// `//` comments and `"..."` strings. Reusing CSharpBracketSearcher gives the
|
||||
// language correct bracket highlighting without a per-grammar implementation.
|
||||
public override ICSharpCode.ILSpy.TextView.IBracketSearcher BracketSearcher { get; } = new CSharpBracketSearcher(); |
||||
|
||||
public override void DecompileMethod(IMethod method, ITextOutput output, DecompilationOptions options) |
||||
{ |
||||
base.DecompileMethod(method, output, options); |
||||
var module = method.ParentModule!.MetadataFile!; |
||||
new ReflectionDisassembler(output, options.CancellationToken) |
||||
.DisassembleMethodHeader(module, (SRM.MethodDefinitionHandle)method.MetadataToken); |
||||
output.WriteLine(); |
||||
output.WriteLine(); |
||||
var methodDef = module.Metadata.GetMethodDefinition((SRM.MethodDefinitionHandle)method.MetadataToken); |
||||
if (!methodDef.HasBody()) |
||||
return; |
||||
var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver()); |
||||
var reader = new ILReader(typeSystem.MainModule); |
||||
var methodBody = module.GetMethodBody(methodDef.RelativeVirtualAddress); |
||||
reader.WriteTypedIL((SRM.MethodDefinitionHandle)method.MetadataToken, methodBody, output, cancellationToken: options.CancellationToken); |
||||
} |
||||
} |
||||
} |
||||
|
||||
#endif
|
||||
Loading…
Reference in new issue