diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs index 6d519c523..5041dbcc9 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs @@ -81,5 +81,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty func(); func2(); } + + public static void CapturedBoolResult(Dictionary 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); + } } } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index a807a74f1..7ef08ee92 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -176,6 +176,8 @@ namespace ICSharpCode.Decompiler.CSharp List astTransforms = GetAstTransforms(); + public Stepper Stepper { get; set; } = new Stepper(); + /// /// Returns all built-in transforms of the C# AST pipeline. /// @@ -714,17 +716,27 @@ namespace ICSharpCode.Decompiler.CSharp 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 }); diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs b/ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs index 20ca149a1..1c65e7004 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs @@ -155,7 +155,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return new InsertedNodeList(a, b); } - public abstract void Insert(); + public abstract void Insert(TransformContext context); } class InsertedNodeList : InsertedNode @@ -168,10 +168,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms this.child2 = child2; } - public override void Insert() + public override void Insert(TransformContext context) { - child1.Insert(); - child2.Insert(); + child1.Insert(context); + child2.Insert(context); } } @@ -186,12 +186,15 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms 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); } } @@ -208,11 +211,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms 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; @@ -222,12 +226,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms 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 @@ -260,11 +265,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Result r = GetResultFromBlock(block); if (context.DecompileRun.Settings.CheckForOverflowUnderflow) { - r.NodesToInsertInCheckedContext?.Insert(); + r.NodesToInsertInCheckedContext?.Insert(context); } else { - r.NodesToInsertInUncheckedContext?.Insert(); + r.NodesToInsertInUncheckedContext?.Insert(context); } } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs index ddc869610..3ccb253ed 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs @@ -49,6 +49,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms string doc = provider.GetDocumentation(entity); if (doc != null) { + context.Step("Add XML documentation", entityDecl); InsertXmlDocumentation(entityDecl, new StringReader(doc)); } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/CombineQueryExpressions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/CombineQueryExpressions.cs index 49496c1e2..a98365095 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/CombineQueryExpressions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/CombineQueryExpressions.cs @@ -19,6 +19,7 @@ #nullable enable using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using ICSharpCode.Decompiler.CSharp.Syntax; @@ -32,11 +33,21 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms /// public class CombineQueryExpressions : IAstTransform { + [AllowNull] TransformContext context; + public void Run(AstNode rootNode, TransformContext context) { if (!context.Settings.QueryExpressions) return; - CombineQueries(rootNode, new Dictionary()); + this.context = context; + try + { + CombineQueries(rootNode, new Dictionary()); + } + finally + { + this.context = null; + } } static readonly InvocationExpression castPattern = new InvocationExpression { @@ -68,10 +79,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms 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 @@ -79,6 +92,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Match m = castPattern.Match(fromClause.Expression); if (m.Success) { + context.Step("Move Cast type into from clause", fromClause); fromClause.Type = m.Get("targetType").Single().Detach(); fromClause.Expression = m.Get("inExpr").Single().Detach(); } @@ -117,6 +131,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms // 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 @@ -125,6 +140,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { query.Clauses.InsertAfter(insertionPos, insertionPos = clause.Detach()); } + context.EndStep(query.Clauses.First()); foreach (var expr in match.Get("expr")) { @@ -176,7 +192,9 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms newIdent.RemoveAnnotations(); // 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; } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs index d9ae39453..8fbc4b9a3 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs @@ -213,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { if (stmt.Expression is DirectionExpression dir && IsValidInStatementExpression(dir.Expression)) { + context.Step("Unwrap direction expression statement", stmt); stmt.Expression = dir.Expression.Detach(); } else if (!IsValidInStatementExpression(stmt.Expression)) @@ -222,12 +223,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms // if possible use C# 7.0 discard-assignment if (context.Settings.Discards && !ExpressionBuilder.HidesVariableWithName(function, "_")) { + context.Step("Assign invalid expression statement to discard", stmt); stmt.Expression = new AssignmentExpression( new IdentifierExpression("_"), // no ResolveResult stmt.Expression.Detach()); } else { + context.Step("Assign invalid expression statement to temporary", stmt); // assign result to dummy variable var type = stmt.Expression.GetResolveResult().Type; var v = function.RegisterVariable( @@ -542,7 +545,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms continue; var designation = StatementBuilder.TranslateDeconstructionDesignation(deconstruct, isForeach: false); - left.ReplaceWith(new DeclarationExpression { Type = new SimpleType("var"), Designation = designation }); + context.Step("Declare deconstruction variables", left); + var declarationExpression = new DeclarationExpression { Type = new SimpleType("var"), Designation = designation }; + left.ReplaceWith(declarationExpression); + context.EndStep(declarationExpression); foreach (var v in usedVariables) { @@ -595,7 +601,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms void InsertVariableDeclarations(TransformContext context) { - var replacements = new List<(AstNode, AstNode)>(); + var replacements = new List<(AstNode OldNode, Func CreateNewNode, string StepDescription)>(); foreach (var (ilVariable, v) in variableDict) { if (v.RemovedDueToCollision || v.DeclaredInDeconstruction) @@ -621,17 +627,19 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { type.AddTrailingTrivia(new Comment("pinned", CommentType.MultiLine)); } - var vds = new VariableDeclarationStatement(type, v.Name, assignment.Right.Detach()); - var init = vds.Variables.Single(); - init.AddAnnotation(assignment.Left.GetResolveResult()); - foreach (object annotation in assignment.Left.Annotations.Concat(assignment.Annotations)) - { - if (!(annotation is ResolveResult)) + replacements.Add((v.InsertionPoint.nextNode, () => { + var vds = new VariableDeclarationStatement(type, v.Name, assignment.Right.Detach()); + var init = vds.Variables.Single(); + init.AddAnnotation(assignment.Left.GetResolveResult()); + foreach (object annotation in assignment.Left.Annotations.Concat(assignment.Annotations)) { - init.AddAnnotation(annotation); + if (!(annotation is ResolveResult)) + { + init.AddAnnotation(annotation); + } } - } - replacements.Add((v.InsertionPoint.nextNode, vds)); + return vds; + }, "Combine variable declaration with initializer")); } else if (CanBeDeclaredAsOutVariable(v, out var dirExpr)) { @@ -675,7 +683,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms ovd.RemoveAnnotations(); ovd.AddAnnotation(new OutVarResolveResult(v.Type)); } - replacements.Add((dirExpr, ovd)); + replacements.Add((dirExpr, () => ovd, "Declare out variable")); } else { @@ -688,6 +696,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } var vds = new VariableDeclarationStatement(type, v.Name, initializer); vds.Variables.Single().AddAnnotation(new ILVariableResolveResult(ilVariable)); + context.Step("Insert variable declaration", v.InsertionPoint.nextNode); if (v.InsertionPoint.nextNode.Parent is LambdaExpression lambda) { Debug.Assert(lambda.Body is not BlockStatement); @@ -708,24 +717,27 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { AstType unsafeType = context.TypeSystemAstBuilder.ConvertType( context.TypeSystem.FindType(KnownTypeCode.Unsafe)); + AstNode insertedNode; if (context.Settings.OutVariables) { var outVarDecl = new OutVarDeclarationExpression(type.Clone(), v.Name); outVarDecl.Variable.AddAnnotation(new ILVariableResolveResult(ilVariable)); + var skipInitStatement = new ExpressionStatement { + Expression = new InvocationExpression { + Target = new MemberReferenceExpression { + Target = new TypeReferenceExpression(unsafeType), + MemberName = "SkipInit" + }, + Arguments = { + outVarDecl + } + } + }; insertionParent.InsertChildBefore( v.InsertionPoint.nextNode, - new ExpressionStatement { - Expression = new InvocationExpression { - Target = new MemberReferenceExpression { - Target = new TypeReferenceExpression(unsafeType), - MemberName = "SkipInit" - }, - Arguments = { - outVarDecl - } - } - }, + skipInitStatement, Slots.Statement); + insertedNode = skipInitStatement; } else { @@ -733,25 +745,28 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms v.InsertionPoint.nextNode, vds, Slots.Statement); + insertedNode = vds; + var skipInitStatement = new ExpressionStatement { + Expression = new InvocationExpression { + Target = new MemberReferenceExpression { + Target = new TypeReferenceExpression(unsafeType), + MemberName = "SkipInit" + }, + Arguments = { + new DirectionExpression( + FieldDirection.Out, + new IdentifierExpression(v.Name) + .WithRR(new ILVariableResolveResult(ilVariable)) + ) + } + } + }; insertionParent.InsertChildBefore( v.InsertionPoint.nextNode, - new ExpressionStatement { - Expression = new InvocationExpression { - Target = new MemberReferenceExpression { - Target = new TypeReferenceExpression(unsafeType), - MemberName = "SkipInit" - }, - Arguments = { - new DirectionExpression( - FieldDirection.Out, - new IdentifierExpression(v.Name) - .WithRR(new ILVariableResolveResult(ilVariable)) - ) - } - } - }, + skipInitStatement, Slots.Statement); } + context.EndStep(insertedNode); } else { @@ -759,13 +774,17 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms v.InsertionPoint.nextNode, vds, Slots.Statement); + context.EndStep(vds); } } } // perform replacements at end, so that we don't replace a node while it is still referenced by a VariableToDeclare - foreach (var (oldNode, newNode) in replacements) + foreach (var (oldNode, createNewNode, stepDescription) in replacements) { + context.Step(stepDescription, oldNode); + var newNode = createNewNode(); oldNode.ReplaceWith(newNode); + context.EndStep(newNode); } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs index cf8a10fa7..67b2dab91 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs @@ -56,7 +56,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { foreach (var ident in rootNode.DescendantsAndSelf.OfType()) { - ident.Name = ReplaceInvalid(ident.Name); + string newName = ReplaceInvalid(ident.Name); + if (newName != ident.Name) + { + context.Step($"Escape identifier '{ident.Name}'", ident); + ident.Name = newName; + } } } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/FixNameCollisions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/FixNameCollisions.cs index 6d75d1ff6..940b5f61f 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/FixNameCollisions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/FixNameCollisions.cs @@ -56,6 +56,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (memberNames.Contains(oldName) && symbol is IField { Accessibility: Accessibility.Private }) { string newName = PickNewName(memberNames, oldName); + context.Step($"Rename field '{oldName}' to '{newName}'", fieldDecl); fieldDecl.Variables.Single().Name = newName; renamedSymbols[symbol] = newName; } @@ -70,6 +71,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (symbol != null && renamedSymbols.TryGetValue(symbol, out string? newName)) { // An IdentifierExpression / MemberReferenceExpression always carries its name identifier. + context.Step($"Rename field reference to '{newName}'", node); node.GetChild(Slots.Identifier)!.Name = newName; } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/FlattenSwitchBlocks.cs b/ICSharpCode.Decompiler/CSharp/Transforms/FlattenSwitchBlocks.cs index 52764aa7a..9c01bff24 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/FlattenSwitchBlocks.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/FlattenSwitchBlocks.cs @@ -22,6 +22,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (blockStatement == null || blockStatement.Statements.Any(ContainsLocalDeclaration)) continue; + context.Step("Flatten switch section block", blockStatement); blockStatement.Remove(); blockStatement.Statements.MoveTo(switchSection.Statements); } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs index 1535260c3..7cb5ac668 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs @@ -108,10 +108,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return; } var method = (IMethod)invocationExpression.GetSymbol()!; + bool stepped = false; if (firstArgument is DirectionExpression dirExpr) { if (!context.Settings.RefExtensionMethods || dirExpr.FieldDirection == FieldDirection.Out) return; + context.Step("Introduce extension method call", invocationExpression); + stepped = true; // A ref/out direction expression always wraps an operand. firstArgument = dirExpr.Expression!; target = firstArgument.GetResolveResult(); @@ -120,17 +123,23 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms else if (firstArgument is NullReferenceExpression) { Debug.Assert(context.RequiredNamespacesSuperset.Contains(method.Parameters[0].Type.Namespace)); + context.Step("Introduce extension method call", invocationExpression); + stepped = true; // The replacement is a freshly created CastExpression, so the result is non-null. firstArgument = firstArgument.ReplaceWith(expr => new CastExpression(context.TypeSystemAstBuilder.ConvertType(method.Parameters[0].Type), expr.Detach()))!; } if (invocationExpression.Target is IdentifierExpression identifierExpression) { + if (!stepped) + context.Step("Introduce extension method call", invocationExpression); identifierExpression.Detach(); memberRefExpr = new MemberReferenceExpression(firstArgument.Detach(), method.Name, identifierExpression.TypeArguments.Detach()); invocationExpression.Target = memberRefExpr; } else { + if (!stepped) + context.Step("Introduce extension method call", invocationExpression); // The target is not an IdentifierExpression, so CanTransformToExtensionMethodCall // matched the MemberReferenceExpression case and memberRefExpr is non-null. memberRefExpr!.Target = firstArgument.Detach(); diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs index 2bd67a7cf..2b07fc7e8 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs @@ -51,6 +51,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (IsDegenerateQuery(query)) { // introduce select for degenerate query + context.Step("Add degenerate query select clause", query); query.Clauses.Add(new QuerySelectClause { Expression = new IdentifierExpression(fromClause.Identifier).CopyAnnotationsFrom(fromClause) }); } // See if the data source of this query is a degenerate query, @@ -61,6 +62,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms QueryFromClause innerFromClause = (QueryFromClause)innerQuery.Clauses.First(); ILVariable? innerVariable = innerFromClause.Annotation()?.Variable; ILVariable? rangeVariable = fromClause.Annotation()?.Variable; + context.Step("Combine nested query clauses", fromClause); // Replace the fromClause with all clauses from the inner query fromClause.Remove(); QueryClause? insertionPos = null; @@ -69,6 +71,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms CombineRangeVariables(clause, innerVariable, rangeVariable); query.Clauses.InsertAfter(insertionPos, insertionPos = clause.Detach()); } + context.EndStep(innerFromClause); fromClause = innerFromClause; innerQuery = fromClause.Expression as QueryExpression; } @@ -87,9 +90,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms var variable = parent.Annotation()?.Variable; if (variable == oldVariable) { + context.Step("Combine query range variables", identifier); parent.RemoveAnnotations(); parent.AddAnnotation(new ILVariableResolveResult(newVariable)); - identifier.ReplaceWith(Identifier.Create(newVariable.Name!)); + var newIdentifier = Identifier.Create(newVariable.Name!); + identifier.ReplaceWith(newIdentifier); + context.EndStep(newIdentifier); } } } @@ -110,6 +116,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (node.Parent is ExpressionStatement && CanUseDiscardAssignment()) query = new AssignmentExpression(new IdentifierExpression("_"), query); node.ReplaceWith(query); + context.EndStep(query); } AstNode? next; @@ -145,6 +152,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Expression expr = invocation.Arguments.Single(); if (MatchSimpleLambda(expr, out var parameter, out var body)) { + context.Step("Build select query", invocation); QueryExpression query = new QueryExpression(); query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach())); query.Clauses.Add(new QuerySelectClause { Expression = WrapExpressionInParenthesesIfNecessary(body.Detach(), parameter.Name!) }.CopyAnnotationsFrom(expr)); @@ -162,6 +170,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms && MatchSimpleLambda(projectionLambda, out var parameter2, out var elementSelector) && parameter1.Name == parameter2.Name) { + context.Step("Build group query", invocation); QueryExpression query = new QueryExpression(); query.Clauses.Add(MakeFromClause(parameter1, mre.Target.Detach())); var queryGroupClause = new QueryGroupClause { @@ -179,6 +188,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Expression lambda = invocation.Arguments.Single(); if (MatchSimpleLambda(lambda, out var parameter, out var keySelector)) { + context.Step("Build group query", invocation); QueryExpression query = new QueryExpression(); query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach())); query.Clauses.Add(new QueryGroupClause { Projection = new IdentifierExpression(parameter.Name!).CopyAnnotationsFrom(parameter), Key = keySelector.Detach() }); @@ -203,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms ParameterDeclaration p2 = lambda.Parameters.ElementAt(1); if (p1.Name == parameter.Name) { + context.Step("Build select-many query", invocation); QueryExpression query = new QueryExpression(); query.Clauses.Add(MakeFromClause(p1, mre.Target.Detach())); query.Clauses.Add(MakeFromClause(p2, collectionSelector.Detach()).CopyAnnotationsFrom(fromExpressionLambda)); @@ -221,6 +232,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Expression expr = invocation.Arguments.Single(); if (MatchSimpleLambda(expr, out var parameter, out var body)) { + context.Step("Build where query", invocation); QueryExpression query = new QueryExpression(); query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach())); query.Clauses.Add(new QueryWhereClause { Condition = body.Detach() }.CopyAnnotationsFrom(expr)); @@ -242,6 +254,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { if (ValidateThenByChain(invocation, parameter.Name!)) { + context.Step("Build order query", invocation); QueryOrderClause orderClause = new QueryOrderClause(); while (mre.MemberName == "ThenBy" || mre.MemberName == "ThenByDescending") { @@ -302,6 +315,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (ValidateParameter(p1) && ValidateParameter(p2) && p1.Name == element1.Name && (p2.Name == element2.Name || mre.MemberName == "GroupJoin")) { + context.Step(mre.MemberName == "GroupJoin" ? "Build group join query" : "Build join query", invocation); QueryExpression query = new QueryExpression(); query.Clauses.Add(MakeFromClause(element1, source1.Detach())); QueryJoinClause joinClause = new QueryJoinClause(); diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs index 6450e412a..ba0040d13 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs @@ -29,9 +29,19 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { public class IntroduceUnsafeModifier : DepthFirstAstVisitor, IAstTransform { + TransformContext? context; + public void Run(AstNode compilationUnit, TransformContext context) { - compilationUnit.AcceptVisitor(this); + this.context = context; + try + { + compilationUnit.AcceptVisitor(this); + } + finally + { + this.context = null; + } } public static bool IsUnsafe(AstNode node) @@ -52,6 +62,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } if (result && node is EntityDeclaration && !(node is Accessor)) { + if (context != null) + context.Step("Add unsafe modifier", node); ((EntityDeclaration)node).Modifiers |= Modifiers.Unsafe; return false; } @@ -95,6 +107,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms && bop.GetResolveResult() is OperatorResolveResult orr && orr.Operands.FirstOrDefault()?.Type.Kind == TypeKind.Pointer) { + if (context != null) + context.Step("Replace pointer addition with indexer", unaryOperatorExpression); // transform "*(ptr + int)" to "ptr[int]" IndexerExpression indexer = new IndexerExpression(); indexer.Target = bop.Left!.Detach(); @@ -102,6 +116,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms indexer.CopyAnnotationsFrom(unaryOperatorExpression); indexer.CopyAnnotationsFrom(bop); unaryOperatorExpression.ReplaceWith(indexer); + if (context != null) + context.EndStep(indexer); } return true; } @@ -121,6 +137,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms UnaryOperatorExpression? uoe = memberReferenceExpression.Target as UnaryOperatorExpression; if (uoe != null && uoe.Operator == UnaryOperatorType.Dereference) { + if (context != null) + context.Step("Replace pointer member access", memberReferenceExpression); PointerReferenceExpression pre = new PointerReferenceExpression(); pre.Target = uoe.Expression.Detach(); pre.MemberName = memberReferenceExpression.MemberName; @@ -129,6 +147,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms pre.RemoveAnnotations(); // only copy the ResolveResult from the MRE pre.CopyAnnotationsFrom(memberReferenceExpression); memberReferenceExpression.ReplaceWith(pre); + if (context != null) + context.EndStep(pre); } if (HasUnsafeResolveResult(memberReferenceExpression)) return true; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs index 020c31038..7144e718f 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs @@ -26,6 +26,9 @@ using System.Linq; using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.CSharp.Syntax; +#if STEP +using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching; +#endif using ICSharpCode.Decompiler.CSharp.TypeSystem; using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.Semantics; @@ -73,7 +76,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { resolvedNamespaces.Add(resolvedNamespace); } - rootNode.InsertChildAfter(insertionPoint, new UsingDeclaration { Import = nsType }, Slots.Member); + context.Step("Add using declaration", rootNode); + var node = new UsingDeclaration { Import = nsType }; + rootNode.InsertChildAfter(insertionPoint, node, Slots.Member); + context.EndStep(node); } } @@ -189,6 +195,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { readonly bool ignoreUsingScope; readonly DecompilerSettings settings; + readonly TransformContext context; CSharpResolver resolver; TypeSystemAstBuilder astBuilder; @@ -197,6 +204,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms public FullyQualifyAmbiguousTypeNamesVisitor(TransformContext context, UsingScope usingScope) { + this.context = context; this.ignoreUsingScope = !context.Settings.UsingDeclarations; this.settings = context.Settings; this.resolver = new CSharpResolver(new CSharpTypeResolveContext(context.TypeSystem.MainModule)); @@ -401,13 +409,31 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } if (simpleType.Parent is Syntax.Attribute) { - simpleType.ReplaceWith(astBuilder.ConvertAttributeType(rr.Type)); + ReplaceAndRecordStep("Qualify ambiguous attribute type", simpleType, astBuilder.ConvertAttributeType(rr.Type)); } else { - simpleType.ReplaceWith(astBuilder.ConvertType(rr.Type)); + ReplaceAndRecordStep("Qualify ambiguous type", simpleType, astBuilder.ConvertType(rr.Type)); } } + + void ReplaceAndRecordStep(string stepDescription, SimpleType simpleType, AstType replacement) + { +#if STEP + // Record a debug step only when the converted type actually differs from the + // original, so the step list stays meaningful. This structural comparison is + // debug-only (it allocates a pattern Match); release builds skip it entirely and + // just perform the replacement below. + bool changed = !simpleType.IsMatch(replacement); + if (changed) + context.Step(stepDescription, simpleType); +#endif + simpleType.ReplaceWith(replacement); +#if STEP + if (changed) + context.EndStep(replacement); +#endif + } } } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs b/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs index b0774e941..5b418b420 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs @@ -26,6 +26,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms base.VisitSyntaxTree(syntaxTree); if (context.Settings.FileScopedNamespaces && singleNamespaceDeclaration != null) { + context.Step("Use file-scoped namespace", singleNamespaceDeclaration); singleNamespaceDeclaration.IsFileScoped = true; } } @@ -106,7 +107,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { if (statement is BlockStatement b && b.Statements.Count == 1 && IsAllowedAsEmbeddedStatement(b.Statements.First(), parent)) { - statement.ReplaceWith(b.Statements.First().Detach()); + context.Step("Remove redundant block statement", statement); + var innerStatement = b.Statements.First().Detach(); + statement.ReplaceWith(innerStatement); + context.EndStep(innerStatement); } else if (!IsAllowedAsEmbeddedStatement(statement, parent)) { @@ -120,11 +124,12 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return parent is IfElseStatement && statement.Slot?.Kind == Slots.FalseStatement; } - static void InsertBlock(Statement statement) + void InsertBlock(Statement statement) { if (!(statement is BlockStatement)) { var b = new BlockStatement(); + context.Step("Add block statement", statement); statement.ReplaceWith(b); if (statement is EmptyStatement && !statement.HasChildren) { @@ -134,6 +139,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { b.Add(statement); } + context.EndStep(b); } } @@ -221,6 +227,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return; if ((getter.Modifiers & ~movableModifiers) != 0) return; + context.Step("Use expression-bodied property", propertyDeclaration); propertyDeclaration.Modifiers |= getter.Modifiers; propertyDeclaration.ExpressionBody = m.Get("expression").Single().Detach(); propertyDeclaration.CopyAnnotationsFrom(getter); @@ -236,6 +243,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return; if ((getter.Modifiers & ~movableModifiers) != 0) return; + context.Step("Use expression-bodied indexer", indexerDeclaration); indexerDeclaration.Modifiers |= getter.Modifiers; indexerDeclaration.ExpressionBody = m.Get("expression").Single().Detach(); indexerDeclaration.CopyAnnotationsFrom(getter); diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs index 2aa550794..1763677fd 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs @@ -191,6 +191,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return null; if (next is ForStatement forStatement && ForStatementUsesVariable(forStatement, variable)) { + context.Step("Move declaration into for initializer", node); node.Remove(); next.InsertChildAfter(null, node, Slots.ForInitializer); return (ForStatement)next; @@ -212,6 +213,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms // Whereas continue in for jumps to the increment block. if (loop.DescendantNodes(DescendIntoStatement).OfType().Any(s => s is ContinueStatement)) return null; + context.Step("Transform while loop to for", loop); node.Remove(); BlockStatement newBody = new BlockStatement(); foreach (Statement stmt in m3.Get("statement")) @@ -223,6 +225,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms forStatement.Iterators.Add(iteratorStatement.Detach()); forStatement.EmbeddedStatement = newBody; loop.ReplaceWith(forStatement); + context.EndStep(forStatement); return forStatement; } @@ -363,6 +366,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return null; if (indexVariable.StoreCount != 2 || indexVariable.LoadCount != 3 || indexVariable.AddressCount != 0) return null; + context.Step("Introduce foreach over array", forStatement); var body = new BlockStatement(); foreach (var statement in m.Get("statements")) body.Statements.Add(statement.Detach()); @@ -378,6 +382,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); // TODO : add ForeachAnnotation forStatement.ReplaceWith(foreachStmt); + context.EndStep(foreachStmt); return foreachStmt; } @@ -533,6 +538,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms || !upperBounds.All(ub => ub.IsSingleDefinition && ub.LoadCount == 1) || !lowerBounds.All(lb => lb.StoreCount == 2 && lb.LoadCount == 3 && lb.AddressCount == 0)) return null; + context.Step("Introduce foreach over multidimensional array", expressionStatement); var body = new BlockStatement(); foreach (var statement in statements) body.Statements.Add(statement.Detach()); @@ -550,6 +556,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); // TODO : add ForeachAnnotation expressionStatement.ReplaceWith(foreachStmt); + context.EndStep(foreachStmt); return foreachStmt; } @@ -643,6 +650,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms return null; if (field.IsCompilerGenerated() && field.DeclaringTypeDefinition == property.DeclaringTypeDefinition) { + context.Step("Convert property to auto-property", propertyDeclaration); // Clearing the accessor body turns it into an auto-property accessor. var getter = propertyDeclaration.Getter; var setter = propertyDeclaration.Setter; @@ -710,6 +718,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (newIdentifier != null) { identifier.ReplaceWith(newIdentifier); + context.EndStep(newIdentifier); return newIdentifier; } } @@ -769,6 +778,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { if (!property.CanSet && !context.Settings.GetterOnlyAutomaticProperties) return null; + context.Step("Replace backing field use with property", identifier); parent.RemoveAnnotations(); parent.AddAnnotation(new MemberResolveResult(mrr.TargetResult, property)); return Identifier.Create(property.Name); @@ -793,6 +803,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms var eventDef = module.ResolveEntity(eventHandle) as IEvent; if (eventDef != null && currentMethod?.AccessorOwner != eventDef) { + context.Step("Replace event backing field use with event", identifier); parent.RemoveAnnotations(); parent.AddAnnotation(new MemberResolveResult(mrr.TargetResult, eventDef)); identifier.Name = eventDef.Name; @@ -1026,6 +1037,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } if (ev.AddAccessor is not { } addAccessor) return null; + context.Step("Convert custom event to field-like event", ev); RemoveCompilerGeneratedAttribute(addAccessor.Attributes, attributeTypesToRemoveFromAutoEvents); EventDeclaration ed = new EventDeclaration(); ev.Attributes.MoveTo(ed.Attributes); @@ -1054,6 +1066,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } ev.ReplaceWith(ed); + context.EndStep(ed); return ed; bool IsEventBackingField(FieldDeclaration fd) @@ -1094,6 +1107,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Match m = destructorPattern.Match(methodDef); if (m.Success) { + context.Step("Convert Finalize method to destructor", methodDef); DestructorDeclaration dd = new DestructorDeclaration(); methodDef.Attributes.MoveTo(dd.Attributes); dd.CopyAnnotationsFrom(methodDef); @@ -1103,6 +1117,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms // has an enclosing type at this point. dd.Name = currentTypeDefinition!.Name; methodDef.ReplaceWith(dd); + context.EndStep(dd); return dd; } return null; @@ -1113,6 +1128,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Match m = destructorBodyPattern.Match(dtorDef.Body); if (m.Success) { + context.Step("Simplify destructor body", dtorDef); dtorDef.Body = m.Get("body").Single().Detach(); return dtorDef; } @@ -1139,6 +1155,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { if (tryCatchFinallyPattern.IsMatch(tryFinally)) { + context.Step("Merge nested try-catch-finally", tryFinally); TryCatchStatement tryCatch = (TryCatchStatement)tryFinally.TryBlock.Statements.Single(); tryFinally.TryBlock = tryCatch.TryBlock.Detach(); tryCatch.CatchClauses.MoveTo(tryFinally.CatchClauses); @@ -1171,6 +1188,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Match m = cascadingIfElsePattern.Match(node); if (m.Success) { + context.Step("Simplify cascading if-else", node); IfElseStatement elseIf = m.Get("nestedIfStatement").Single(); node.FalseStatement = elseIf.Detach(); } @@ -1191,6 +1209,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms var bAndC = expr.Right as BinaryOperatorExpression; if (bAndC != null && bAndC.Operator == expr.Operator) { + context.Step("Reassociate conditional logic", expr); // make bAndC the parent and expr the child. // A conditional-and/or operator always has both operands present. var b = bAndC.Left!.Detach(); @@ -1199,6 +1218,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms bAndC.Left = expr; bAndC.Right = c; expr.Right = b; + context.EndStep(bAndC); return base.VisitBinaryOperatorExpression(bAndC); } break; @@ -1210,8 +1230,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { if (expr.Operator == UnaryOperatorType.Not && expr.Expression is BinaryOperatorExpression { Operator: BinaryOperatorType.Equality } binary) { + context.Step("Replace negated equality with inequality", expr); binary.Operator = BinaryOperatorType.InEquality; expr.ReplaceWith(binary.Detach()); + context.EndStep(binary); return VisitBinaryOperatorExpression(binary); } return base.VisitUnaryOperatorExpression(expr); @@ -1240,6 +1262,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Expression target = m.Get("target").Single(); if (target.GetResolveResult().Type.IsReferenceType == false) { + context.Step("Use pattern-based fixed statement", fixedStatement); v.Initializer = target.Detach(); } } @@ -1262,6 +1285,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (!(usingStatement.ResourceAcquisition is VariableDeclarationStatement)) return usingStatement; + context.Step("Use enhanced using statement", usingStatement); usingStatement.IsEnhanced = true; return usingStatement; } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs index 3b1ad8a20..8681bd72d 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs @@ -63,9 +63,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (CanConvertToCompoundAssignment(assignment.Left) && assignment.Left.IsMatch(binary.Left) && binary.Right != null && IsImplicitlyConvertible(binary.Right, expectedType)) { - assignment.Operator = GetAssignmentOperatorForBinaryOperator(binary.Operator); - if (assignment.Operator != AssignmentOperatorType.Assign) + var newOperator = GetAssignmentOperatorForBinaryOperator(binary.Operator); + if (newOperator != AssignmentOperatorType.Assign) { + context.Step("Convert assignment to compound assignment", assignment); + assignment.Operator = newOperator; // If we found a shorter operator, get rid of the BinaryOperatorExpression: assignment.CopyAnnotationsFrom(binary); assignment.Right = binary.Right; @@ -88,7 +90,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms type = (assignment.Operator == AssignmentOperatorType.Add) ? UnaryOperatorType.PostIncrement : UnaryOperatorType.PostDecrement; else type = (assignment.Operator == AssignmentOperatorType.Add) ? UnaryOperatorType.Increment : UnaryOperatorType.Decrement; - assignment.ReplaceWith(new UnaryOperatorExpression(type, assignment.Left.Detach()).CopyAnnotationsFrom(assignment)); + context.Step(type is UnaryOperatorType.Increment or UnaryOperatorType.PostIncrement ? "Convert assignment to increment" : "Convert assignment to decrement", assignment); + var unaryOperator = new UnaryOperatorExpression(type, assignment.Left.Detach()).CopyAnnotationsFrom(assignment); + assignment.ReplaceWith(unaryOperator); + context.EndStep(unaryOperator); } } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs b/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs index fa0612156..0fecc7cb4 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs @@ -75,6 +75,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms bool isInExpressionTree = invocationExpression.Ancestors.OfType().Any( lambda => lambda.Annotation()?.Kind == IL.ILFunctionKind.ExpressionTree); + context.Step("Replace String.Concat with +", invocationExpression); Expression arg0 = arguments[0].Detach(); Expression arg1 = arguments[1].Detach(); if (!isInExpressionTree) @@ -97,6 +98,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } expr.CopyAnnotationsFrom(invocationExpression); invocationExpression.ReplaceWith(expr); + context.EndStep(expr); return; } @@ -107,9 +109,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { if (typeHandleOnTypeOfPattern.IsMatch(arguments[0])) { + context.Step("Replace GetTypeFromHandle with typeof", invocationExpression); Expression target = ((MemberReferenceExpression)arguments[0]).Target; target.CopyInstructionsFrom(invocationExpression); invocationExpression.ReplaceWith(target); + context.EndStep(target); return; } } @@ -147,15 +151,20 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms method.TypeArguments.Count == 1 && IsInstantiableTypeParameter(method.TypeArguments[0])) { - invocationExpression.ReplaceWith(new ObjectCreateExpression(context.TypeSystemAstBuilder.ConvertType(method.TypeArguments.First()))); + context.Step("Replace Activator.CreateInstance with new", invocationExpression); + var objectCreate = new ObjectCreateExpression(context.TypeSystemAstBuilder.ConvertType(method.TypeArguments.First())); + invocationExpression.ReplaceWith(objectCreate); + context.EndStep(objectCreate); } break; case "System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray": if (arguments.Length == 2 && context.Settings.Ranges) { + context.Step("Replace RuntimeHelpers.GetSubArray with range indexer", invocationExpression); var slicing = new IndexerExpression(arguments[0].Detach(), arguments[1].Detach()); slicing.CopyAnnotationsFrom(invocationExpression); invocationExpression.ReplaceWith(slicing); + context.EndStep(slicing); } break; } @@ -164,6 +173,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms BinaryOperatorType? bop = GetBinaryOperatorTypeFromMetadataName(method.Name, out isChecked, context.Settings); if (bop != null && arguments.Length == 2) { + context.Step("Replace operator method with binary operator", invocationExpression); invocationExpression.Arguments.Clear(); // detach arguments from invocationExpression if (isChecked) { @@ -173,13 +183,13 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { invocationExpression.AddAnnotation(AddCheckedBlocks.UncheckedAnnotation); } - invocationExpression.ReplaceWith( - new BinaryOperatorExpression( - arguments[0].UnwrapInDirectionExpression(), - bop.Value, - arguments[1].UnwrapInDirectionExpression() - ).CopyAnnotationsFrom(invocationExpression) - ); + var binaryOperator = new BinaryOperatorExpression( + arguments[0].UnwrapInDirectionExpression(), + bop.Value, + arguments[1].UnwrapInDirectionExpression() + ).CopyAnnotationsFrom(invocationExpression); + invocationExpression.ReplaceWith(binaryOperator); + context.EndStep(binaryOperator); return; } UnaryOperatorType? uop = GetUnaryOperatorTypeFromMetadataName(method.Name, out isChecked, context.Settings); @@ -199,28 +209,31 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms // because it doesn't assign the incremented value to a. if (method.DeclaringType.IsKnownType(KnownTypeCode.Decimal)) { + context.Step("Replace decimal increment method with arithmetic", invocationExpression); // Legacy csc optimizes "d + 1m" to "op_Increment(d)", // so reverse that optimization here: - invocationExpression.ReplaceWith( - new BinaryOperatorExpression( - arguments[0].UnwrapInDirectionExpression().Detach(), - (uop == UnaryOperatorType.Increment ? BinaryOperatorType.Add : BinaryOperatorType.Subtract), - new PrimitiveExpression(1m) - ).CopyAnnotationsFrom(invocationExpression) - ); + var arithmetic = new BinaryOperatorExpression( + arguments[0].UnwrapInDirectionExpression().Detach(), + (uop == UnaryOperatorType.Increment ? BinaryOperatorType.Add : BinaryOperatorType.Subtract), + new PrimitiveExpression(1m) + ).CopyAnnotationsFrom(invocationExpression); + invocationExpression.ReplaceWith(arithmetic); + context.EndStep(arithmetic); } } else { + context.Step("Replace operator method with unary operator", invocationExpression); arguments[0].Remove(); // detach argument - invocationExpression.ReplaceWith( - new UnaryOperatorExpression(uop.Value, arguments[0].UnwrapInDirectionExpression()).CopyAnnotationsFrom(invocationExpression) - ); + var unaryOperator = new UnaryOperatorExpression(uop.Value, arguments[0].UnwrapInDirectionExpression()).CopyAnnotationsFrom(invocationExpression); + invocationExpression.ReplaceWith(unaryOperator); + context.EndStep(unaryOperator); } return; } if (method.Name is "op_Explicit" or "op_CheckedExplicit" && arguments.Length == 1) { + context.Step("Replace conversion operator method with cast", invocationExpression); arguments[0].Remove(); // detach argument if (method.Name == "op_CheckedExplicit") { @@ -230,15 +243,18 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { invocationExpression.AddAnnotation(AddCheckedBlocks.UncheckedAnnotation); } - invocationExpression.ReplaceWith( - new CastExpression(context.TypeSystemAstBuilder.ConvertType(method.ReturnType), arguments[0].UnwrapInDirectionExpression()) - .CopyAnnotationsFrom(invocationExpression) - ); + var cast = new CastExpression(context.TypeSystemAstBuilder.ConvertType(method.ReturnType), arguments[0].UnwrapInDirectionExpression()) + .CopyAnnotationsFrom(invocationExpression); + invocationExpression.ReplaceWith(cast); + context.EndStep(cast); return; } if (method.Name == "op_True" && arguments.Length == 1 && invocationExpression.Slot?.Kind == Slots.Condition) { - invocationExpression.ReplaceWith(arguments[0].UnwrapInDirectionExpression()); + context.Step("Remove op_True from condition", invocationExpression); + var condition = arguments[0].UnwrapInDirectionExpression(); + invocationExpression.ReplaceWith(condition); + context.EndStep(condition); return; } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs index 505587ef8..bb8d8cb96 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs @@ -19,9 +19,11 @@ #nullable enable using System.Collections.Immutable; +using System.Diagnostics; using System.Threading; using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -36,6 +38,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms public readonly TypeSystemAstBuilder TypeSystemAstBuilder; public readonly DecompilerSettings Settings; internal readonly DecompileRun DecompileRun; + public Stepper Stepper { get; set; } readonly ITypeResolveContext decompilationContext; @@ -67,6 +70,74 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms this.TypeSystemAstBuilder = typeSystemAstBuilder; this.CancellationToken = decompileRun.CancellationToken; this.Settings = decompileRun.Settings; + this.Stepper = new Stepper(); + } + + /// + /// Call this method immediately before performing a transform step. + /// Unlike context.Stepper.Step(), calls to this method are only compiled in debug builds. + /// + [Conditional("STEP")] + [DebuggerStepThrough] + internal void Step(string description, AstNode? near = null) + { + TrackModifiedNode(Stepper.Step(description, modifiedNode: near), near); + } + + [Conditional("STEP")] + [DebuggerStepThrough] + internal void StepStartGroup(string description, AstNode? near = null) + { + TrackModifiedNode(Stepper.StartGroup(description, modifiedNode: near), near); + } + + [Conditional("STEP")] + internal void StepEndGroup(bool keepIfEmpty = false) + { + Stepper.EndGroup(keepIfEmpty); + } + + /// + /// Points the most recently recorded step at the node its mutation produced. + /// Call this after a whose modified node only comes into existence + /// during the mutation (e.g. the result of a ReplaceWith or a freshly inserted node). + /// + [Conditional("STEP")] + internal void EndStep(AstNode? modifiedNode) + { + if (Stepper.LastStep != null) + { + Stepper.LastStep.ModifiedNode = modifiedNode; + TrackModifiedNode(Stepper.LastStep, modifiedNode, insertFirst: true); + } + } + + static void TrackModifiedNode(Stepper.Node step, AstNode? modifiedNode, bool insertFirst = false) + { + if (modifiedNode == null) + return; + AddCandidate(step, modifiedNode, insertFirst); + var marker = new DebugStepMarker(); + modifiedNode.AddAnnotation(marker); + AddCandidate(step, marker, insertFirst: false); + for (var parent = modifiedNode.Parent; parent != null; parent = parent.Parent) + { + AddCandidate(step, parent, insertFirst: false); + } + } + + static void AddCandidate(Stepper.Node step, object candidate, bool insertFirst) + { + if (step.ModifiedNodeCandidates.Contains(candidate)) + return; + if (insertFirst) + step.ModifiedNodeCandidates.Insert(0, candidate); + else + step.ModifiedNodeCandidates.Add(candidate); + } + + sealed class DebugStepMarker + { } } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs index 404109102..d817decd3 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs @@ -481,6 +481,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms var ci = new ConstructorInitializer { ConstructorInitializerType = type }; + context.Step("Move constructor call to initializer", stmt); // Move arguments from invocation to initializer: invocation.GetChildren(Slots.Argument).MoveTo(ci.Arguments); // Add the initializer: (unless it is the default 'base()') @@ -489,6 +490,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms // Remove the statement stmt.Remove(); + context.EndStep(constructorDeclaration.Initializer); return true; } @@ -507,7 +509,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms // e.g. when a single static constructor is decompiled in isolation -- so the // assignment must remain in the constructor body. if (kind is InitializerKind.Primary) + { + context.Step("Remove redundant primary constructor assignment", stmt); stmt.Remove(); + } continue; } @@ -518,7 +523,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms v = fd.Variables.Single(); if (v.Initializer is null) { - v.Initializer = initializer.Detach(); + context.Step("Move assignment to field initializer", stmt); + var movedInitializer = initializer.Detach(); + v.Initializer = movedInitializer; + context.EndStep(movedInitializer); } else if (kind == InitializerKind.Static) { @@ -548,7 +556,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms Debug.Assert(pd.IsAutomaticProperty); if (pd.Initializer is null) { - pd.Initializer = initializer.Detach(); + context.Step("Move assignment to property initializer", stmt); + var movedInitializer = initializer.Detach(); + pd.Initializer = movedInitializer; + context.EndStep(movedInitializer); } else { @@ -560,7 +571,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms v = ev.Variables.Single(); if (v.Initializer is null) { - v.Initializer = initializer.Detach(); + context.Step("Move assignment to event initializer", stmt); + var movedInitializer = initializer.Detach(); + v.Initializer = movedInitializer; + context.EndStep(movedInitializer); } else { @@ -594,6 +608,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (sequence.IsUnsafe && IntroduceUnsafeModifier.IsUnsafe(initializer)) { + context.Step("Add unsafe modifier to initialized member", declaringSyntaxNode); declaringSyntaxNode.Modifiers |= Modifiers.Unsafe; } } @@ -626,6 +641,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms var insertionPoint = (AstNode?)this.TypeDeclaration.TypeParameters.LastOrDefault() ?? this.TypeDeclaration.NameToken; foreach (var param in PrimaryConstructorDecl.Parameters) { + context.Step("Move primary constructor parameter to type", param); param.Remove(); this.TypeDeclaration.InsertChildAfter(insertionPoint, param, Slots.Parameter); insertionPoint = param; @@ -680,6 +696,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (PrimaryConstructorDecl.HasModifier(Modifiers.Unsafe)) { + context.Step("Move unsafe modifier from primary constructor to type", this.TypeDeclaration); this.TypeDeclaration.Modifiers |= Modifiers.Unsafe; } @@ -689,11 +706,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms var baseType = TypeDeclaration.BaseTypes.First(); var newBaseType = new InvocationAstType(); + context.Step("Move primary constructor initializer to base type", baseType); baseType.ReplaceWith(newBaseType); newBaseType.BaseType = baseType; initializer.Arguments.MoveTo(newBaseType.Arguments); + context.EndStep(newBaseType); } + context.Step("Remove primary constructor body", PrimaryConstructorDecl); PrimaryConstructorDecl.Remove(); } @@ -704,6 +724,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms if (IsBeforeFieldInit && StaticConstructorDecl.Body is { Statements.Count: 0 }) { + context.Step("Remove empty static constructor", StaticConstructorDecl); StaticConstructorDecl.Remove(); } } @@ -733,7 +754,10 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms bool retainBecauseOfDocumentation = context.Settings.ShowXmlDocumentation && context.DecompileRun.DocumentationProvider?.GetDocumentation(ctorMethod) != null; if (!retainBecauseOfDocumentation) + { + context.Step("Remove implicit constructor", ctor); ctor.Remove(); + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs index abbdddbf1..7976749bc 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs @@ -54,6 +54,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms } public IList Steps => steps; + public Node? LimitReachedStep { get; private set; } + public Node? LastStep { get; private set; } public int StepLimit { get; set; } = int.MaxValue; public bool IsDebug { get; set; } @@ -62,6 +64,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms { public string Description { get; } public ILInstruction? Position { get; set; } + public object? ModifiedNode { get; set; } + public IList ModifiedNodeCandidates { get; } = new List(); /// /// BeginStep is inclusive. /// @@ -96,39 +100,62 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// May throw in debug mode. /// [DebuggerStepThrough] - public void Step(string description, ILInstruction? near = null) + public Node Step(string description, ILInstruction? near = null, object? modifiedNode = null) { - StepInternal(description, near); + return StepInternal(description, near, modifiedNode ?? near); } [DebuggerStepThrough] - private Node StepInternal(string description, ILInstruction? near) + private Node StepInternal(string description, ILInstruction? near, object? modifiedNode) { + var stepNode = new Node($"{step}: {description}") { + Position = near, + ModifiedNode = modifiedNode, + BeginStep = step, + EndStep = step + 1 + }; if (step == StepLimit) { + LimitReachedStep = stepNode; if (IsDebug) Debugger.Break(); else throw new StepLimitReachedException(); } - var stepNode = new Node($"{step}: {description}") { - Position = near, - BeginStep = step, - EndStep = step + 1 - }; var p = groups.PeekOrDefault(); if (p != null) p.Children.Add(stepNode); else steps.Add(stepNode); + LastStep = stepNode; step++; return stepNode; } [DebuggerStepThrough] - public void StartGroup(string description, ILInstruction? near = null) + public Node StartGroup(string description, ILInstruction? near = null, object? modifiedNode = null) + { + var stepNode = StepInternal(description, near, modifiedNode ?? near); + groups.Push(stepNode); + return stepNode; + } + + public Node? GetStepByBeginStep(int beginStep) { - groups.Push(StepInternal(description, near)); + return FindStep(steps, beginStep); + + static Node? FindStep(IEnumerable nodes, int beginStep) + { + foreach (var node in nodes) + { + if (node.BeginStep == beginStep) + return node; + var child = FindStep(node.Children, beginStep); + if (child != null) + return child; + } + return null; + } } public void EndGroup(bool keepIfEmpty = false) diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 6f719e10a..f836062df 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -18,6 +18,7 @@ #if DEBUG +using System; using System.Linq; using System.Threading.Tasks; @@ -27,10 +28,14 @@ using Avalonia.VisualTree; using AwesomeAssertions; +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.Syntax; + using ICSharpCode.ILSpy; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Docking; using ICSharpCode.ILSpy.Languages; +using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.Views; @@ -119,6 +124,75 @@ public class DebugStepsTests "after switching to ILAst and decompiling, the VM's Steps must list the stepper's recorded transforms"); } + [AvaloniaTest] + public async Task CSharp_DebugSteps_Are_Grouped_By_Ast_Transform() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var languageService = AppComposition.Current.GetExport(); + var csharp = languageService.Languages.OfType().First(); + languageService.CurrentLanguage = csharp; + + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + typeNode.IsExpanded = true; + var method = typeNode.Children.OfType() + .First(m => m.MethodDefinition.Name == "Range"); + vm.AssemblyTreeModel.SelectNode(method); + await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + var debugStepsVm = AppComposition.Current.GetExport(); + await Waiters.WaitForAsync( + () => debugStepsVm.Steps?.Count > 0, + description: "DebugStepsPaneModel.Steps to be populated after the C# decompile"); + + var astTransformNames = CSharpDecompiler.GetAstTransforms() + .Select(transform => transform.GetType().Name) + .ToArray(); + + debugStepsVm.Steps! + .Select(step => StripStepNumber(step.Description)) + .Should().Equal(astTransformNames, + "C# debug steps must use AST transforms as top-level groups"); + + var transformGroupWithChanges = debugStepsVm.Steps! + .FirstOrDefault(step => step.Children.Count > 0); + transformGroupWithChanges.Should().NotBeNull( + "individual C# AST mutation steps must be nested under their transform group"); + transformGroupWithChanges!.Children + .Select(step => StripStepNumber(step.Description)) + .Should().Contain( + description => !astTransformNames.Contains(description), + "nested C# debug steps must describe individual AST mutation points"); + + var collectedSteps = debugStepsVm.Steps; + var replayStep = transformGroupWithChanges.Children.First(); + var tab = vm.DockWorkspace.ActiveDecompilerTab!; + + tab.RestartDecompileWithStepLimit(replayStep.BeginStep, isDebug: false, replayStep.BeginStep); + tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + tab.Text.Should().NotBeNullOrWhiteSpace("C# replay before a selected AST mutation step must still emit code"); + tab.DebugStepHighlight.Should().NotBeNull("C# replay before a selected AST mutation step must locate the changed node"); + debugStepsVm.Steps.Should().BeSameAs(collectedSteps, + "a step-limited C# replay must not replace the full step tree shown by the pane"); + + tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep); + tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + tab.Text.Should().NotBeNullOrWhiteSpace("C# replay after a selected AST mutation step must still emit code"); + tab.DebugStepHighlight.Should().NotBeNull("C# replay after a selected AST mutation step must locate the changed node"); + debugStepsVm.Steps.Should().BeSameAs(collectedSteps, + "a step-limited C# replay must preserve the current full-run step tree and selection context"); + + static string StripStepNumber(string description) + { + var separatorIndex = description.IndexOf(": ", StringComparison.Ordinal); + return separatorIndex >= 0 ? description[(separatorIndex + 2)..] : description; + } + } + [AvaloniaTest] public Task ILAst_And_TypedIL_Languages_Are_Registered_In_Debug_Builds() { @@ -133,6 +207,25 @@ public class DebugStepsTests languageService.Languages.Should().Contain(l => l.Name == "Typed IL"); return Task.CompletedTask; } + + [AvaloniaTest] + public Task NodeLookup_Resolves_Copied_Ast_Annotations() + { + var marker = new object(); + var original = new IdentifierExpression("old"); + original.AddAnnotation(marker); + var replacement = new IdentifierExpression("new").CopyAnnotationsFrom(original); + var lookup = new NodeLookup(); + + lookup.AddNode(replacement, 12, 3); + + lookup.TryGetRange(marker, out var range).Should().BeTrue( + "C# debug-step markers copied by AST replacements must still resolve to emitted text"); + range.Start.Should().Be(12); + range.Length.Should().Be(3); + return Task.CompletedTask; + } + [AvaloniaTest] public Task Pane_Reports_Not_Available_For_Languages_Without_Debug_Steps() { diff --git a/ILSpy/DecompilationOptions.cs b/ILSpy/DecompilationOptions.cs index 1c205b61c..2e076ec24 100644 --- a/ILSpy/DecompilationOptions.cs +++ b/ILSpy/DecompilationOptions.cs @@ -60,6 +60,13 @@ namespace ICSharpCode.ILSpy /// public int StepLimit { get; set; } = int.MaxValue; + /// + /// Step whose changed node should be highlighted after a debug-stepper re-decompile. + /// This can differ from when showing the state after a step, + /// because the next recorded step is where the pipeline stops. + /// + public int? HighlightStep { get; set; } + /// /// When true, transforms emit verbose debug information about their behaviour. Only /// meaningful in combination with — the Debug Steps pane sets diff --git a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs index eb84056d1..09684fcfd 100644 --- a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs +++ b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs @@ -124,6 +124,12 @@ namespace ICSharpCode.ILSpy.Languages //this.externAliasKeywordColor = ...; } + public CSharpHighlightingTokenWriter(TokenWriter decoratedWriter, AvaloniaEditTextOutput textOutput, ILocatable? locatable = null) + : this(decoratedWriter, (ISmartTextOutput?)textOutput, locatable) + { + this.nodeTrackingOutput = textOutput; + } + public override void WriteKeyword(string keyword) { HighlightingColor? color = null; @@ -496,6 +502,7 @@ namespace ICSharpCode.ILSpy.Languages public override void StartNode(AstNode node) { + nodeTrackingOutput?.MarkNodeStart(node); nodeStack.Push(node); base.StartNode(node); } @@ -503,6 +510,7 @@ namespace ICSharpCode.ILSpy.Languages public override void EndNode(AstNode node) { base.EndNode(node); + nodeTrackingOutput?.MarkNodeEnd(node); nodeStack.Pop(); } @@ -511,6 +519,7 @@ namespace ICSharpCode.ILSpy.Languages int currentColorBegin = -1; readonly ILocatable? locatable; readonly ISmartTextOutput? textOutput; + readonly AvaloniaEditTextOutput? nodeTrackingOutput; // Wraps a base WriteX call so its output lands inside a highlighting span for the given colour // (or no span when null) -- replacing the begin/end guard each WriteX override used to repeat. diff --git a/ILSpy/Languages/CSharpLanguage.DebugSteps.cs b/ILSpy/Languages/CSharpLanguage.DebugSteps.cs index a852d6d8a..13b5c6a27 100644 --- a/ILSpy/Languages/CSharpLanguage.DebugSteps.cs +++ b/ILSpy/Languages/CSharpLanguage.DebugSteps.cs @@ -32,18 +32,16 @@ using ICSharpCode.ILSpy.ViewModels; namespace ICSharpCode.ILSpy.Languages { /// - /// Debug Steps support for the C# language: coarse, one step per AST transform, shown in the - /// Debug Steps pane like the ILAst language already does for IL transforms. The step list is - /// static -- the C# AST pipeline () is the same - /// for every member -- so a selected step's index maps straight onto , which CreateDecompiler turns into the number of AST - /// transforms to keep before re-rendering. + /// Debug Steps support for the C# language, shown in the Debug Steps pane like the ILAst + /// language already does for IL transforms. A full decompile records AST transform groups with + /// individual mutation steps inside; a selected step's index is replayed by re-decompiling with . /// partial class CSharpLanguage : IDebugStepProvider { - Stepper? stepper; + Stepper stepper = new Stepper(); - public Stepper Stepper => stepper ??= BuildStepper(); + public Stepper Stepper => stepper; public event EventHandler? StepperUpdated; @@ -51,18 +49,7 @@ namespace ICSharpCode.ILSpy.Languages // tree for C#, unlike ILAst's writing-options checkboxes. public object? StepOptions => null; - // One node per AST transform, in pipeline order. Stepper.Step assigns BeginStep=i / - // EndStep=i+1 automatically, so "show state before step i" re-decompiles with StepLimit=i - // (run i transforms) and "after" with StepLimit=i+1 -- exactly the cap CreateDecompiler wants. - static Stepper BuildStepper() - { - var s = new Stepper(); - foreach (var transform in CSharpDecompiler.GetAstTransforms()) - s.Step(transform.GetType().Name); - return s; - } - - partial void OnCSharpDecompiled(ITextOutput output, DecompilationOptions options) + partial void OnCSharpDecompiled(CSharpDecompiler decompiler, ITextOutput output, DecompilationOptions options) { // The button always shows so the pane is one click away; mirrors the ILAst language. // DockWorkspace is resolved lazily (an ImportingConstructor import would form a MEF @@ -74,7 +61,7 @@ namespace ICSharpCode.ILSpy.Languages // pane itself) must leave the tree and the user's selection intact. if (options.StepLimit == int.MaxValue) { - _ = Stepper; + stepper = decompiler.Stepper; StepperUpdated?.Invoke(this, EventArgs.Empty); } } diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 73a2fcc0a..cf6ca92f5 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -33,6 +33,7 @@ using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.IL; +using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.Solution; @@ -313,11 +314,8 @@ namespace ICSharpCode.ILSpy.Languages CancellationToken = options.CancellationToken, DebugInfoProvider = module.GetDebugInfoOrNull(), }; - // The Debug Steps pane stops the AST pipeline at a chosen step by re-decompiling with - // options.StepLimit = number of AST transforms to keep; pop the rest from the end. - // StepLimit is int.MaxValue for a normal decompile, so nothing is removed. - while (decompiler.AstTransforms.Count > options.StepLimit) - decompiler.AstTransforms.RemoveAt(decompiler.AstTransforms.Count - 1); + decompiler.Stepper.StepLimit = options.StepLimit; + decompiler.Stepper.IsDebug = options.IsDebug; if (options.EscapeInvalidIdentifiers) decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); return decompiler; @@ -345,25 +343,25 @@ namespace ICSharpCode.ILSpy.Languages { var members = CollectFieldsAndCtors(methodDefinition.DeclaringTypeDefinition!, methodDefinition.IsStatic); decompiler.AstTransforms.Add(new SelectCtorTransform(methodDefinition)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(members)); + WriteCode(output, options, decompiler.Decompile(members), decompiler); } else { - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(method.MetadataToken)); + WriteCode(output, options, decompiler.Decompile(method.MetadataToken), decompiler); } - OnCSharpDecompiled(output, options); + OnCSharpDecompiled(decompiler, output, options); } // Implemented only under DEBUG (CSharpLanguage.DebugSteps.cs) to feed the Debug Steps pane; // a no-op partial in Release. - partial void OnCSharpDecompiled(ITextOutput output, DecompilationOptions options); + partial void OnCSharpDecompiled(CSharpDecompiler decompiler, ITextOutput output, DecompilationOptions options); public override void DecompileProperty(IProperty property, ITextOutput output, DecompilationOptions options) { CSharpDecompiler decompiler = BeginDecompile(property, output, options); WriteCommentLine(output, TypeToString(property.DeclaringType)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(property.MetadataToken)); - OnCSharpDecompiled(output, options); + WriteCode(output, options, decompiler.Decompile(property.MetadataToken), decompiler); + OnCSharpDecompiled(decompiler, output, options); } public override void DecompileField(IField field, ITextOutput output, DecompilationOptions options) @@ -372,16 +370,16 @@ namespace ICSharpCode.ILSpy.Languages WriteCommentLine(output, TypeToString(field.DeclaringType)); if (field.IsConst) { - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(field.MetadataToken)); + WriteCode(output, options, decompiler.Decompile(field.MetadataToken), decompiler); } else { var members = CollectFieldsAndCtors(field.DeclaringTypeDefinition!, field.IsStatic); var resolvedField = decompiler.TypeSystem.MainModule.GetDefinition((FieldDefinitionHandle)field.MetadataToken); decompiler.AstTransforms.Add(new SelectFieldTransform(resolvedField)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(members)); + WriteCode(output, options, decompiler.Decompile(members), decompiler); } - OnCSharpDecompiled(output, options); + OnCSharpDecompiled(decompiler, output, options); } /// @@ -408,24 +406,24 @@ namespace ICSharpCode.ILSpy.Languages CSharpDecompiler decompiler = BeginDecompile(extension, output, options); WriteCommentLine(output, TypeToString(commentType, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames | ConversionFlags.SupportExtensionDeclarations)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.DecompileExtension(extension.MetadataToken)); - OnCSharpDecompiled(output, options); + WriteCode(output, options, decompiler.DecompileExtension(extension.MetadataToken), decompiler); + OnCSharpDecompiled(decompiler, output, options); } public override void DecompileEvent(IEvent ev, ITextOutput output, DecompilationOptions options) { CSharpDecompiler decompiler = BeginDecompile(ev, output, options); WriteCommentLine(output, TypeToString(ev.DeclaringType)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(ev.MetadataToken)); - OnCSharpDecompiled(output, options); + WriteCode(output, options, decompiler.Decompile(ev.MetadataToken), decompiler); + OnCSharpDecompiled(decompiler, output, options); } public override void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options) { CSharpDecompiler decompiler = BeginDecompile(type, output, options); WriteCommentLine(output, TypeToString(type, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(type.MetadataToken)); - OnCSharpDecompiled(output, options); + WriteCode(output, options, decompiler.Decompile(type.MetadataToken), decompiler); + OnCSharpDecompiled(decompiler, output, options); } public override ProjectId? DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) @@ -511,7 +509,7 @@ namespace ICSharpCode.ILSpy.Languages SyntaxTree st = options.FullDecompilation ? decompiler.DecompileWholeModuleAsSingleFile() : decompiler.DecompileModuleAndAssemblyAttributes(); - WriteCode(output, options.DecompilerSettings, decompiler, st); + WriteCode(output, options, st, decompiler); return null; } @@ -709,13 +707,15 @@ namespace ICSharpCode.ILSpy.Languages } } - static void WriteCode(ITextOutput output, DecompilerSettings settings, CSharpDecompiler decompiler, SyntaxTree syntaxTree) + static void WriteCode(ITextOutput output, DecompilationOptions options, SyntaxTree syntaxTree, CSharpDecompiler decompiler) { + var settings = options.DecompilerSettings; syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); output.IndentationString = settings.CSharpFormattingOptions.IndentationString; - TokenWriter tokenWriter = new TextTokenWriter(output, settings, decompiler.TypeSystem); - if (output is TextView.ISmartTextOutput smartOutput) + if (output is TextView.AvaloniaEditTextOutput avaloniaOutput) + tokenWriter = new CSharpHighlightingTokenWriter(tokenWriter, avaloniaOutput); + else if (output is TextView.ISmartTextOutput smartOutput) tokenWriter = new CSharpHighlightingTokenWriter(tokenWriter, smartOutput); // For the on-screen C# view, harvest the IL-offset/line map for body bookmarks during this @@ -728,6 +728,47 @@ namespace ICSharpCode.ILSpy.Languages syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); bookmarkCollector?.Publish(); + if (output is TextView.AvaloniaEditTextOutput nodeOutput + && TryGetDebugStepHighlightRange(decompiler, options, nodeOutput.NodeLookup, out var range)) + { + nodeOutput.DebugStepHighlight = range; + } + } + + static bool TryGetDebugStepHighlightRange(CSharpDecompiler decompiler, DecompilationOptions options, TextView.NodeLookup nodeLookup, out TextView.TextRange range) + { + range = default; + if (options.StepLimit == int.MaxValue) + return false; + if (options.HighlightStep is { } highlightStep) + { + if (TryGetRange(decompiler.Stepper.GetStepByBeginStep(highlightStep), nodeLookup, out range)) + return true; + if (decompiler.Stepper.LimitReachedStep is { BeginStep: var limitStep } reachedStep + && limitStep == highlightStep) + { + return TryGetRange(reachedStep, nodeLookup, out range); + } + return false; + } + if (TryGetRange(decompiler.Stepper.LimitReachedStep, nodeLookup, out range)) + return true; + if (options.StepLimit > 0) + return TryGetRange(decompiler.Stepper.GetStepByBeginStep(options.StepLimit - 1), nodeLookup, out range); + return false; + + static bool TryGetRange(Stepper.Node? step, TextView.NodeLookup nodeLookup, out TextView.TextRange range) + { + range = default; + if (step == null) + return false; + foreach (var candidate in step.ModifiedNodeCandidates) + { + if (nodeLookup.TryGetRange(candidate, out range)) + return true; + } + return step.ModifiedNode != null && nodeLookup.TryGetRange(step.ModifiedNode, out range); + } } void AddWarningMessage(MetadataFile module, ITextOutput output, string line1, string? line2 = null, diff --git a/ILSpy/TextView/AvaloniaEditTextOutput.cs b/ILSpy/TextView/AvaloniaEditTextOutput.cs index 9c04fcf48..6fa7f1747 100644 --- a/ILSpy/TextView/AvaloniaEditTextOutput.cs +++ b/ILSpy/TextView/AvaloniaEditTextOutput.cs @@ -54,6 +54,7 @@ namespace ICSharpCode.ILSpy.TextView public int LengthLimit { get; set; } = int.MaxValue; readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new(); + readonly Stack<(object Node, int Offset)> openNodes = new(); readonly Stack<(NewFolding Folding, int StartLine)> openFoldings = new(); readonly List foldings = new(); int indent; @@ -87,6 +88,10 @@ namespace ICSharpCode.ILSpy.TextView /// Maps reference targets to their definition offsets in the rendered text. public DefinitionLookup DefinitionLookup { get; } = new(); + internal NodeLookup NodeLookup { get; } = new(); + + internal TextRange? DebugStepHighlight { get; set; } + readonly List methodDebugInfos = new(); /// @@ -300,5 +305,20 @@ namespace ICSharpCode.ILSpy.TextView highlightingSpans.Add((start, length, color)); } } + + internal void MarkNodeStart(object node) + { + openNodes.Push((node, builder.Length)); + } + + internal void MarkNodeEnd(object node) + { + if (openNodes.Count == 0) + return; + var (currentNode, start) = openNodes.Pop(); + if (!ReferenceEquals(currentNode, node)) + return; + NodeLookup.AddNode(node, start, builder.Length - start); + } } } diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index 397dff4b4..4091e3012 100644 --- a/ILSpy/TextView/DecompilerTabPageModel.cs +++ b/ILSpy/TextView/DecompilerTabPageModel.cs @@ -183,6 +183,9 @@ namespace ICSharpCode.ILSpy.TextView [ObservableProperty] private DefinitionLookup? definitionLookup; + [ObservableProperty] + private TextRange? debugStepHighlight; + /// /// IL-offset <-> line maps for the methods in this document (C# only). Lets bookmarks /// anchor in-method lines by IL offset and the gutter place their icons. Null for non-C# @@ -379,6 +382,7 @@ namespace ICSharpCode.ILSpy.TextView // every run; set non-default by RestartDecompileWithStepLimit before kicking off a // debug-stepper decompile. Set on the UI thread only — no inter-thread access. int pendingStepLimit = int.MaxValue; + int? pendingHighlightStep; bool pendingIsDebug; /// @@ -397,9 +401,10 @@ namespace ICSharpCode.ILSpy.TextView /// is ). toggles the transforms' /// verbose-debug emission. No-op when there's nothing currently being decompiled. /// - public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug) + public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug, int? highlightStep = null) { pendingStepLimit = stepLimit; + pendingHighlightStep = highlightStep; pendingIsDebug = isDebug; StartDecompile(); } @@ -511,6 +516,7 @@ namespace ICSharpCode.ILSpy.TextView References = null; DefinitionLookup = null; DebugInfo = null; + DebugStepHighlight = null; UIElements = null; Text = string.Empty; IsDecompiling = false; @@ -537,8 +543,10 @@ namespace ICSharpCode.ILSpy.TextView // stable value, and reset the fields to defaults so the NEXT decompile runs // at full fidelity unless RestartDecompileWithStepLimit sets them again. var stepLimit = pendingStepLimit; + var highlightStep = pendingHighlightStep; var isDebug = pendingIsDebug; pendingStepLimit = int.MaxValue; + pendingHighlightStep = null; pendingIsDebug = false; var outputLengthLimit = pendingOutputLengthLimit; pendingOutputLengthLimit = DefaultOutputLengthLimit; @@ -553,6 +561,7 @@ namespace ICSharpCode.ILSpy.TextView decompilerSettings ?? new ICSharpCode.Decompiler.DecompilerSettings()) { CancellationToken = cts.Token, StepLimit = stepLimit, + HighlightStep = highlightStep, IsDebug = isDebug, }; try @@ -745,6 +754,7 @@ namespace ICSharpCode.ILSpy.TextView : output.MethodDebugInfos.Count > 0 ? new Bookmarks.DecompiledDebugInfo(output.MethodDebugInfos) : Bookmarks.DecompiledDebugInfo.Empty; + DebugStepHighlight = output.DebugStepHighlight; UIElements = output.UIElements; Text = text; } diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index 54eb45e70..48b0e3d13 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -64,6 +64,7 @@ namespace ICSharpCode.ILSpy.TextView // softer green for the actual definition. static readonly Color LocalMatchBackground = Colors.GreenYellow; static readonly Color LocalDefinitionBackground = Color.FromArgb(0x80, 0xA0, 0xFF, 0xA0); + static readonly Color DebugStepBackground = Color.FromArgb(0x80, 0xFF, 0xD7, 0x66); // Stay-open corridor for the rich popup: as long as the pointer is closer than // `distanceToPopupLimit` to the popup edges, the popup stays. The limit shrinks toward @@ -78,6 +79,7 @@ namespace ICSharpCode.ILSpy.TextView TextMarkerService textMarkerService = null!; BracketHighlightRenderer bracketHighlightRenderer = null!; readonly List localReferenceMarks = new(); + readonly List debugStepMarks = new(); readonly List activeCustomGenerators = new(); RichTextColorizer? activeColorizer; FoldingManager? activeFoldingManager; @@ -1043,6 +1045,7 @@ namespace ICSharpCode.ILSpy.TextView // force-close even if the popup currently wants to stay (mouseClick: true). TryCloseExistingPopup(mouseClick: true); ClearLocalReferenceMarks(); + ClearDebugStepMarks(); Editor.SyntaxHighlighting = HighlightingService.GetByExtension(model.SyntaxExtension); Editor.Document.Text = model.Text; @@ -1072,6 +1075,8 @@ namespace ICSharpCode.ILSpy.TextView if (restoreViewState) RestoreOrResetViewState(pendingState); + ApplyDebugStepHighlight(model.DebugStepHighlight); + // Position at a navigated-to bookmark once its document (and debug map) has landed. Only on // the final content (the Text change, where restoreViewState is set), AFTER the view-state // reset above, so the bookmark scroll is the last word on position and the highlight plays @@ -1246,6 +1251,31 @@ namespace ICSharpCode.ILSpy.TextView localReferenceMarks.Clear(); } + void ApplyDebugStepHighlight(TextRange? range) + { + ClearDebugStepMarks(); + if (range is not { } r || r.Length <= 0) + return; + var start = Math.Clamp(r.Start, 0, Editor.Document.TextLength); + var end = Math.Clamp(r.Start + r.Length, start, Editor.Document.TextLength); + if (end <= start) + return; + var mark = textMarkerService.Create(start, end - start); + mark.BackgroundColor = DebugStepBackground; + debugStepMarks.Add(mark); + Editor.TextArea.Caret.Offset = start; + Editor.TextArea.Caret.BringCaretToView(); + CaretHighlightAdorner.DisplayCaretHighlightAnimation(Editor.TextArea); + } + + + void ClearDebugStepMarks() + { + foreach (var mark in debugStepMarks) + textMarkerService.Remove(mark); + debugStepMarks.Clear(); + } + // Live cursor + full ScrollOffset, queried at hover-event time. Returns null if the // pointer is past the end of the line so we don't snap back to the last visual // column when the user is hovering empty trailing space. diff --git a/ILSpy/TextView/NodeLookup.cs b/ILSpy/TextView/NodeLookup.cs new file mode 100644 index 000000000..b68f0f4ba --- /dev/null +++ b/ILSpy/TextView/NodeLookup.cs @@ -0,0 +1,50 @@ +// 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. + +using System.Collections.Generic; + +using ICSharpCode.Decompiler.CSharp.Syntax; + +namespace ICSharpCode.ILSpy.TextView +{ + /// + /// Maps syntax tree nodes to character ranges in the rendered text. + /// + internal sealed class NodeLookup + { + readonly Dictionary nodes = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRange(object node, out TextRange range) + => nodes.TryGetValue(node, out range); + + public void AddNode(object node, int start, int length) + { + if (length > 0) + { + nodes[node] = new TextRange(start, length); + if (node is IAnnotatable annotatable) + { + foreach (var annotation in annotatable.Annotations) + { + nodes[annotation] = new TextRange(start, length); + } + } + } + } + } +} diff --git a/ILSpy/TextView/TextRange.cs b/ILSpy/TextView/TextRange.cs new file mode 100644 index 000000000..2eadc7426 --- /dev/null +++ b/ILSpy/TextView/TextRange.cs @@ -0,0 +1,22 @@ +// 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. + +namespace ICSharpCode.ILSpy.TextView +{ + public readonly record struct TextRange(int Start, int Length); +} diff --git a/ILSpy/ViewModels/DebugStepsPaneModel.cs b/ILSpy/ViewModels/DebugStepsPaneModel.cs index f58c10767..b0dc7ff70 100644 --- a/ILSpy/ViewModels/DebugStepsPaneModel.cs +++ b/ILSpy/ViewModels/DebugStepsPaneModel.cs @@ -109,8 +109,8 @@ namespace ICSharpCode.ILSpy.ViewModels { Id = PaneContentId; Title = "Debug Steps"; - ShowStateBeforeCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: false)); - ShowStateAfterCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.EndStep ?? int.MaxValue, isDebug: false)); + ShowStateBeforeCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: false, SelectedStep?.BeginStep)); + ShowStateAfterCommand = new RelayCommand(() => RequestRedecompile(SelectedStep?.EndStep ?? int.MaxValue, isDebug: false, SelectedStep?.BeginStep)); DebugStepCommand = new RelayCommand(() => { // "Debug this step" relies on Stepper.Step calling Debugger.Break() when // step == StepLimit — which is a silent no-op without a debugger attached. @@ -123,7 +123,7 @@ namespace ICSharpCode.ILSpy.ViewModels if (!System.Diagnostics.Debugger.Launch()) AppEnv.AppLog.Mark("DebugStep: Debugger.Launch returned false; the upcoming Stepper.Step break is a no-op without a debugger attached."); } - RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: true); + RequestRedecompile(SelectedStep?.BeginStep ?? int.MaxValue, isDebug: true, SelectedStep?.BeginStep); }); } @@ -222,12 +222,12 @@ namespace ICSharpCode.ILSpy.ViewModels RequestRedecompile(lastSelectedStep, isDebug: false); } - void RequestRedecompile(int stepLimit, bool isDebug) + void RequestRedecompile(int stepLimit, bool isDebug, int? highlightStep = null) { lastSelectedStep = stepLimit; // Composition unavailable in design-time previews; the gesture is a no-op there. var dock = AppComposition.TryGetExport(); - dock?.ActiveDecompilerTab?.RestartDecompileWithStepLimit(stepLimit, isDebug); + dock?.ActiveDecompilerTab?.RestartDecompileWithStepLimit(stepLimit, isDebug, highlightStep); } } }