Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d22d2cf
Add fine-grained C# AST debug steps
siegfriedpammer Jun 27, 2026
e46f6ac
Center C# debug-step highlights
siegfriedpammer Jun 27, 2026
bbe2b6a
Highlight and scroll to the changed instruction in the ILAst view
siegfriedpammer Jun 28, 2026
dddaada
Await debug-step replay decompiles to fix flaky UI tests
siegfriedpammer Jun 28, 2026
e173a82
Exclude leading indentation from highlight position ranges
siegfriedpammer Jun 28, 2026
1d7f6f3
Center debug-step highlight via fold-aware bookmark helper
siegfriedpammer Jul 1, 2026
a85e520
Skip debug-step node tracking on normal decompiles
siegfriedpammer Jul 1, 2026
f176f50
Point a caret at the gap for debug steps that remove a node
siegfriedpammer Jul 1, 2026
7469de4
Add a filter box to the Debug Steps pane
siegfriedpammer Jul 1, 2026
b728685
Bracket ILAst WriteTo in one place via WriteToCore
siegfriedpammer Jul 1, 2026
c418c6a
Bridge only the debug-step marker in NodeLookup
siegfriedpammer Jul 1, 2026
e1f83f6
Record candidates for the C# step that hits the step limit
siegfriedpammer Jul 1, 2026
954ab83
Assert debug-step replay highlights are precise, not just present
siegfriedpammer Jul 1, 2026
564cbdc
Clear debug steps synchronously on selection to avoid a race
siegfriedpammer Jul 1, 2026
0ef33b8
Ignore null C# debug-step produced nodes
siegfriedpammer Jul 1, 2026
5f9d776
Guard deferred debug-step highlight scrolling
siegfriedpammer Jul 1, 2026
1f9db28
Disable debug-step node tracking by default
siegfriedpammer Jul 1, 2026
b0ac40c
Handle debug-step Enter before tree items consume it
siegfriedpammer Jul 1, 2026
9389a9d
Consolidate unsafe-modifier step guards
siegfriedpammer Jul 1, 2026
9f6c920
Share debug-step candidate recording between IL and C#
siegfriedpammer Jul 1, 2026
832e0b8
Move debug-step stepping to a neutral namespace
siegfriedpammer Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void MarkFoldEnd() { }
var syntaxTree = decompiler.DecompileType(new FullTypeName(type.FullName));

var output = new ReferenceRecordingOutput();
var tokenWriter = new TextTokenWriter(output, settings, decompiler.TypeSystem);
var tokenWriter = new TextTokenWriter(output, settings);
syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions));
return output.MemberReferences;
}
Expand Down
11 changes: 11 additions & 0 deletions ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,16 @@ public void M5()
func();
func2();
}

public static void CapturedBoolResult(Dictionary<int, int> d, int key)
{
// The boolean result of the out-returning call is captured into a local, yet the out
// parameter is still promoted to an inline 'out var' rather than a separate declaration.
bool value = d.TryGetValue(key, out var value2);
Console.WriteLine(value);
Console.WriteLine(value);
Console.WriteLine(value2);
Console.WriteLine(value2);
}
}
}
27 changes: 20 additions & 7 deletions ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.CSharp.TypeSystem;
using ICSharpCode.Decompiler.DebugSteps;
using ICSharpCode.Decompiler.DebugInfo;
using ICSharpCode.Decompiler.Disassembler;
using ICSharpCode.Decompiler.Documentation;
Expand Down Expand Up @@ -176,6 +177,8 @@ public static List<IILTransform> GetILTransforms()

List<IAstTransform> astTransforms = GetAstTransforms();

public Stepper Stepper { get; set; } = new Stepper();

/// <summary>
/// Returns all built-in transforms of the C# AST pipeline.
/// </summary>
Expand Down Expand Up @@ -714,17 +717,27 @@ DecompileRun CreateDecompileRun(HashSet<string> namespaces)
void RunTransforms(AstNode rootNode, DecompileRun decompileRun, ITypeResolveContext decompilationContext)
{
var typeSystemAstBuilder = CreateAstBuilder(decompileRun.Settings);
var context = new TransformContext(typeSystem, decompileRun, decompilationContext, typeSystemAstBuilder);
var context = new TransformContext(typeSystem, decompileRun, decompilationContext, typeSystemAstBuilder) {
Stepper = Stepper
};
// The tree handed to the pipeline must already be well-formed; check it once up front so a
// malformed builder output is caught here rather than blamed on the first transform (DEBUG only).
rootNode.CheckInvariant();
foreach (var transform in astTransforms)
try
{
foreach (var transform in astTransforms)
{
CancellationToken.ThrowIfCancellationRequested();
context.StepStartGroup(transform.GetType().Name);
transform.Run(rootNode, context);
// Verify the slot structure survived the transform (DEBUG only); mirrors the IL
// pipeline's per-transform ILInstruction.CheckInvariant.
rootNode.CheckInvariant();
context.StepEndGroup(keepIfEmpty: true);
}
}
catch (StepLimitReachedException)
{
CancellationToken.ThrowIfCancellationRequested();
transform.Run(rootNode, context);
// Verify the slot structure survived the transform (DEBUG only); mirrors the IL
// pipeline's per-transform ILInstruction.CheckInvariant.
rootNode.CheckInvariant();
}
CancellationToken.ThrowIfCancellationRequested();
rootNode.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true });
Expand Down
33 changes: 19 additions & 14 deletions ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ abstract class InsertedNode
return new InsertedNodeList(a, b);
}

public abstract void Insert();
public abstract void Insert(TransformContext context);
}

class InsertedNodeList : InsertedNode
Expand All @@ -168,10 +168,10 @@ public InsertedNodeList(AddCheckedBlocks.InsertedNode child1, AddCheckedBlocks.I
this.child2 = child2;
}

public override void Insert()
public override void Insert(TransformContext context)
{
child1.Insert();
child2.Insert();
child1.Insert(context);
child2.Insert(context);
}
}

Expand All @@ -186,12 +186,15 @@ public InsertedExpression(Expression expression, bool isChecked)
this.isChecked = isChecked;
}

public override void Insert()
public override void Insert(TransformContext context)
{
context.Step(isChecked ? "Add checked expression" : "Add unchecked expression", expression);
Expression? replacement;
if (isChecked)
expression.ReplaceWith(e => new CheckedExpression { Expression = e });
replacement = expression.ReplaceWith(e => new CheckedExpression { Expression = e });
else
expression.ReplaceWith(e => new UncheckedExpression { Expression = e });
replacement = expression.ReplaceWith(e => new UncheckedExpression { Expression = e });
context.EndStep(replacement);
}
}

Expand All @@ -208,11 +211,12 @@ public InsertedBlock(Statement? firstStatement, Statement? lastStatement, bool i
this.isChecked = isChecked;
}

public override void Insert()
public override void Insert(TransformContext context)
{
// An InsertedBlock with a null start has infinite cost in the search and is never
// selected for insertion, so by the time Insert runs firstStatement is non-null.
Debug.Assert(firstStatement != null);
context.Step(isChecked ? "Add checked block" : "Add unchecked block", firstStatement);
BlockStatement newBlock = new BlockStatement();
// Move all statements except for the first
Statement? next;
Expand All @@ -222,12 +226,13 @@ public override void Insert()
newBlock.Add(stmt.Detach());
}
// Replace the first statement with the new (un)checked block
if (isChecked)
firstStatement.ReplaceWith(new CheckedStatement { Body = newBlock });
else
firstStatement.ReplaceWith(new UncheckedStatement { Body = newBlock });
Statement checkedBlock = isChecked
? new CheckedStatement { Body = newBlock }
: new UncheckedStatement { Body = newBlock };
firstStatement.ReplaceWith(checkedBlock);
// now also move the first node into the new block
newBlock.Statements.InsertAfter(null, firstStatement);
context.EndStep(checkedBlock);
}
}
#endregion
Expand Down Expand Up @@ -260,11 +265,11 @@ public void Run(AstNode node, TransformContext context)
Result r = GetResultFromBlock(block);
if (context.DecompileRun.Settings.CheckForOverflowUnderflow)
{
r.NodesToInsertInCheckedContext?.Insert();
r.NodesToInsertInCheckedContext?.Insert(context);
}
else
{
r.NodesToInsertInUncheckedContext?.Insert();
r.NodesToInsertInUncheckedContext?.Insert(context);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public void Run(AstNode rootNode, TransformContext context)
string doc = provider.GetDocumentation(entity);
if (doc != null)
{
context.Step("Add XML documentation", entityDecl);
InsertXmlDocumentation(entityDecl, new StringReader(doc));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#nullable enable

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

using ICSharpCode.Decompiler.CSharp.Syntax;
Expand All @@ -32,11 +33,21 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
/// </summary>
public class CombineQueryExpressions : IAstTransform
{
[AllowNull] TransformContext context;

public void Run(AstNode rootNode, TransformContext context)
{
if (!context.Settings.QueryExpressions)
return;
CombineQueries(rootNode, new Dictionary<string, object?>());
this.context = context;
try
{
CombineQueries(rootNode, new Dictionary<string, object?>());
}
finally
{
this.context = null;
}
}

static readonly InvocationExpression castPattern = new InvocationExpression {
Expand Down Expand Up @@ -68,17 +79,20 @@ void CombineQueries(AstNode node, Dictionary<string, object?> fromOrLetIdentifie
else
{
QueryContinuationClause continuation = new QueryContinuationClause();
context.Step("Introduce query continuation", fromClause);
continuation.PrecedingQuery = innerQuery.Detach();
continuation.Identifier = fromClause.Identifier;
continuation.CopyAnnotationsFrom(fromClause);
fromClause.ReplaceWith(continuation);
context.EndStep(continuation);
}
}
else
{
Match m = castPattern.Match(fromClause.Expression);
if (m.Success)
{
context.Step("Move Cast type into from clause", fromClause);
fromClause.Type = m.Get<AstType>("targetType").Single().Detach();
fromClause.Expression = m.Get<Expression>("inExpr").Single().Detach();
}
Expand Down Expand Up @@ -117,6 +131,7 @@ bool TryRemoveTransparentIdentifier(QueryExpression query, QueryFromClause fromC
// from * in (from x in ... select new { members of anonymous type }) ...
// =>
// from x in ... { let x = ... } ...
context.Step("Remove transparent query identifier", fromClause);
fromClause.Remove();
selectClause.Remove();
// Move clauses from innerQuery to query
Expand All @@ -125,6 +140,7 @@ bool TryRemoveTransparentIdentifier(QueryExpression query, QueryFromClause fromC
{
query.Clauses.InsertAfter(insertionPos, insertionPos = clause.Detach());
}
context.EndStep(query.Clauses.First());

foreach (var expr in match.Get<Expression>("expr"))
{
Expand Down Expand Up @@ -176,7 +192,9 @@ void RemoveTransparentIdentifierReferences(AstNode node, Dictionary<string, obje
newIdent.RemoveAnnotations<Semantics.MemberResolveResult>(); // remove the reference to the property of the anonymous type
if (fromOrLetIdentifiers.TryGetValue(mre.MemberName, out var annotation) && annotation != null)
newIdent.AddAnnotation(annotation);
context.Step("Replace transparent query identifier reference", mre);
mre.ReplaceWith(newIdent);
context.EndStep(newIdent);
return;
}
}
Expand Down
Loading
Loading