diff --git a/ICSharpCode.Decompiler.Tests/Output/TextTokenWriterTests.cs b/ICSharpCode.Decompiler.Tests/Output/TextTokenWriterTests.cs index 336e61593b..e9db64ecf9 100644 --- a/ICSharpCode.Decompiler.Tests/Output/TextTokenWriterTests.cs +++ b/ICSharpCode.Decompiler.Tests/Output/TextTokenWriterTests.cs @@ -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; } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs index 6d519c5231..5041dbcc91 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OutVariables.cs @@ -81,5 +81,16 @@ public void M5() 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 a807a74f11..0fd1294a1d 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -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; @@ -176,6 +177,8 @@ public static List GetILTransforms() List astTransforms = GetAstTransforms(); + public Stepper Stepper { get; set; } = new Stepper(); + /// /// Returns all built-in transforms of the C# AST pipeline. /// @@ -714,17 +717,27 @@ DecompileRun CreateDecompileRun(HashSet 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 }); diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs b/ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs index 20ca149a1e..1c65e70042 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/AddCheckedBlocks.cs @@ -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 @@ -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); } } @@ -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); } } @@ -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; @@ -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 @@ -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); } } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs index ddc869610b..3ccb253ed3 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/AddXmlDocumentationTransform.cs @@ -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)); } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/CombineQueryExpressions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/CombineQueryExpressions.cs index 49496c1e24..a983650959 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 @@ void CombineQueries(AstNode node, Dictionary 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 @@ -79,6 +92,7 @@ void CombineQueries(AstNode node, Dictionary fromOrLetIdentifie 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 @@ 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 @@ -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("expr")) { @@ -176,7 +192,9 @@ void RemoveTransparentIdentifierReferences(AstNode node, Dictionary(); // 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 d9ae39453e..8fbc4b9a32 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs @@ -213,6 +213,7 @@ void EnsureExpressionStatementsAreValid(AstNode rootNode) { 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 @@ void EnsureExpressionStatementsAreValid(AstNode rootNode) // 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 @@ private void InsertDeconstructionVariableDeclarations() 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 @@ bool CombineDeclarationAndInitializer(VariableToDeclare v, TransformContext cont 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 @@ void InsertVariableDeclarations(TransformContext context) { 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 @@ void InsertVariableDeclarations(TransformContext context) ovd.RemoveAnnotations(); ovd.AddAnnotation(new OutVarResolveResult(v.Type)); } - replacements.Add((dirExpr, ovd)); + replacements.Add((dirExpr, () => ovd, "Declare out variable")); } else { @@ -688,6 +696,7 @@ void InsertVariableDeclarations(TransformContext context) } 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 @@ void InsertVariableDeclarations(TransformContext context) { 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 @@ void InsertVariableDeclarations(TransformContext context) 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 @@ void InsertVariableDeclarations(TransformContext context) 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 cf8a10fa7c..67b2dab919 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs @@ -56,7 +56,12 @@ public void Run(AstNode rootNode, TransformContext context) { 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 6d75d1ff67..940b5f61f0 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/FixNameCollisions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/FixNameCollisions.cs @@ -56,6 +56,7 @@ public void Run(AstNode rootNode, TransformContext context) 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 @@ public void Run(AstNode rootNode, TransformContext context) 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 52764aa7a6..9c01bff24b 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/FlattenSwitchBlocks.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/FlattenSwitchBlocks.cs @@ -22,6 +22,7 @@ public void Run(AstNode rootNode, TransformContext context) 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 1535260c39..7cb5ac6681 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs @@ -108,10 +108,13 @@ public override void VisitInvocationExpression(InvocationExpression invocationEx 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 @@ public override void VisitInvocationExpression(InvocationExpression invocationEx 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 2bd67a7cf9..2b07fc7e87 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs @@ -51,6 +51,7 @@ public void Run(AstNode rootNode, TransformContext context) 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 @@ public void Run(AstNode rootNode, TransformContext context) 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 @@ public void Run(AstNode rootNode, TransformContext context) 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 @@ private void CombineRangeVariables(QueryClause clause, ILVariable? oldVariable, 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 @@ void DecompileQueries(AstNode node) if (node.Parent is ExpressionStatement && CanUseDiscardAssignment()) query = new AssignmentExpression(new IdentifierExpression("_"), query); node.ReplaceWith(query); + context.EndStep(query); } AstNode? next; @@ -145,6 +152,7 @@ bool CanUseDiscardAssignment() 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 @@ bool CanUseDiscardAssignment() && 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 @@ bool CanUseDiscardAssignment() 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 @@ bool CanUseDiscardAssignment() 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 @@ bool CanUseDiscardAssignment() 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 @@ bool CanUseDiscardAssignment() { 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 @@ bool CanUseDiscardAssignment() 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 6450e412a2..7ce802b597 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs @@ -18,6 +18,7 @@ #nullable enable +using System.Diagnostics; using System.Linq; using ICSharpCode.Decompiler.CSharp.Resolver; @@ -29,9 +30,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) @@ -39,6 +50,23 @@ public static bool IsUnsafe(AstNode node) return node.AcceptVisitor(new IntroduceUnsafeModifier()); } + // Run() sets the context, but the static IsUnsafe() entry point drives this visitor with no + // context, so step recording must tolerate a null context. Both helpers compile out entirely + // in non-STEP (Release) builds, so the null check only exists in debug step-recording builds. + [Conditional("STEP")] + void Step(string description, AstNode node) + { + if (context != null) + context.Step(description, node); + } + + [Conditional("STEP")] + void EndStep(AstNode node) + { + if (context != null) + context.EndStep(node); + } + protected override bool VisitChildren(AstNode node) { bool result = false; @@ -52,6 +80,7 @@ protected override bool VisitChildren(AstNode node) } if (result && node is EntityDeclaration && !(node is Accessor)) { + Step("Add unsafe modifier", node); ((EntityDeclaration)node).Modifiers |= Modifiers.Unsafe; return false; } @@ -95,6 +124,7 @@ public override bool VisitUnaryOperatorExpression(UnaryOperatorExpression unaryO && bop.GetResolveResult() is OperatorResolveResult orr && orr.Operands.FirstOrDefault()?.Type.Kind == TypeKind.Pointer) { + Step("Replace pointer addition with indexer", unaryOperatorExpression); // transform "*(ptr + int)" to "ptr[int]" IndexerExpression indexer = new IndexerExpression(); indexer.Target = bop.Left!.Detach(); @@ -102,6 +132,7 @@ public override bool VisitUnaryOperatorExpression(UnaryOperatorExpression unaryO indexer.CopyAnnotationsFrom(unaryOperatorExpression); indexer.CopyAnnotationsFrom(bop); unaryOperatorExpression.ReplaceWith(indexer); + EndStep(indexer); } return true; } @@ -121,6 +152,7 @@ public override bool VisitMemberReferenceExpression(MemberReferenceExpression me UnaryOperatorExpression? uoe = memberReferenceExpression.Target as UnaryOperatorExpression; if (uoe != null && uoe.Operator == UnaryOperatorType.Dereference) { + Step("Replace pointer member access", memberReferenceExpression); PointerReferenceExpression pre = new PointerReferenceExpression(); pre.Target = uoe.Expression.Detach(); pre.MemberName = memberReferenceExpression.MemberName; @@ -129,6 +161,7 @@ public override bool VisitMemberReferenceExpression(MemberReferenceExpression me pre.RemoveAnnotations(); // only copy the ResolveResult from the MRE pre.CopyAnnotationsFrom(memberReferenceExpression); memberReferenceExpression.ReplaceWith(pre); + EndStep(pre); } if (HasUnsafeResolveResult(memberReferenceExpression)) return true; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs index 020c31038d..7144e718fc 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs @@ -26,6 +26,9 @@ 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 @@ public void Run(AstNode rootNode, TransformContext context) { 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 @@ sealed class FullyQualifyAmbiguousTypeNamesVisitor : DepthFirstAstVisitor { readonly bool ignoreUsingScope; readonly DecompilerSettings settings; + readonly TransformContext context; CSharpResolver resolver; TypeSystemAstBuilder astBuilder; @@ -197,6 +204,7 @@ sealed class FullyQualifyAmbiguousTypeNamesVisitor : DepthFirstAstVisitor 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 @@ public override void VisitSimpleType(SimpleType simpleType) } 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 b0774e9419..5b418b4202 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/NormalizeBlockStatements.cs @@ -26,6 +26,7 @@ public override void VisitSyntaxTree(SyntaxTree syntaxTree) base.VisitSyntaxTree(syntaxTree); if (context.Settings.FileScopedNamespaces && singleNamespaceDeclaration != null) { + context.Step("Use file-scoped namespace", singleNamespaceDeclaration); singleNamespaceDeclaration.IsFileScoped = true; } } @@ -106,7 +107,10 @@ void DoTransform(Statement? statement, Statement parent) { 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 @@ bool IsElseIf(Statement statement, Statement parent) 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 @@ static void InsertBlock(Statement statement) { b.Add(statement); } + context.EndStep(b); } } @@ -221,6 +227,7 @@ void SimplifyPropertyDeclaration(PropertyDeclaration propertyDeclaration) 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 @@ void SimplifyIndexerDeclaration(IndexerDeclaration indexerDeclaration) 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 2aa550794c..1763677fd4 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs @@ -191,6 +191,7 @@ public override AstNode VisitTryCatchStatement(TryCatchStatement tryCatchStateme 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 @@ public override AstNode VisitTryCatchStatement(TryCatchStatement tryCatchStateme // 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 @@ public override AstNode VisitTryCatchStatement(TryCatchStatement tryCatchStateme forStatement.Iterators.Add(iteratorStatement.Detach()); forStatement.EmbeddedStatement = newBody; loop.ReplaceWith(forStatement); + context.EndStep(forStatement); return forStatement; } @@ -363,6 +366,7 @@ static bool AddressUsedForSingleCall(IL.ILVariable v, IL.BlockContainer? loop) 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 @@ static bool AddressUsedForSingleCall(IL.ILVariable v, IL.BlockContainer? loop) foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); // TODO : add ForeachAnnotation forStatement.ReplaceWith(foreachStmt); + context.EndStep(foreachStmt); return foreachStmt; } @@ -533,6 +538,7 @@ bool MatchForeachOnMultiDimArray(IL.ILVariable[] upperBounds, IL.ILVariable coll || !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 @@ bool MatchForeachOnMultiDimArray(IL.ILVariable[] upperBounds, IL.ILVariable coll foreachStmt.VariableDesignation.AddAnnotation(new ILVariableResolveResult(itemVariable, itemVariable.Type)); // TODO : add ForeachAnnotation expressionStatement.ReplaceWith(foreachStmt); + context.EndStep(foreachStmt); return foreachStmt; } @@ -643,6 +650,7 @@ bool CanTransformToAutomaticProperty(IProperty property, bool accessorsMustBeCom 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 @@ public override AstNode VisitIdentifier(Identifier identifier) if (newIdentifier != null) { identifier.ReplaceWith(newIdentifier); + context.EndStep(newIdentifier); return newIdentifier; } } @@ -769,6 +778,7 @@ static bool NameCouldBeBackingFieldOfAutomaticProperty(string name, [NotNullWhen { 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 @@ static bool NameCouldBeBackingFieldOfAutomaticProperty(string name, [NotNullWhen 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 @@ bool CheckAutomaticEventV4MCS(CustomEventDeclaration ev) } 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 @@ bool CheckAutomaticEventV4MCS(CustomEventDeclaration ev) } ev.ReplaceWith(ed); + context.EndStep(ed); return ed; bool IsEventBackingField(FieldDeclaration fd) @@ -1094,6 +1107,7 @@ bool IsEventBackingField(FieldDeclaration fd) 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 @@ bool IsEventBackingField(FieldDeclaration fd) // 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 @@ bool IsEventBackingField(FieldDeclaration fd) 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 @@ bool IsEventBackingField(FieldDeclaration fd) { 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 @@ bool IsEventBackingField(FieldDeclaration fd) 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 @@ public override AstNode VisitBinaryOperatorExpression(BinaryOperatorExpression e 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 @@ public override AstNode VisitBinaryOperatorExpression(BinaryOperatorExpression e bAndC.Left = expr; bAndC.Right = c; expr.Right = b; + context.EndStep(bAndC); return base.VisitBinaryOperatorExpression(bAndC); } break; @@ -1210,8 +1230,10 @@ public override AstNode VisitUnaryOperatorExpression(UnaryOperatorExpression exp { 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 @@ public override AstNode VisitFixedStatement(FixedStatement fixedStatement) 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 @@ public override AstNode VisitUsingStatement(UsingStatement usingStatement) 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 3b1ad8a20c..8681bd72dd 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PrettifyAssignments.cs @@ -63,9 +63,11 @@ public override void VisitAssignmentExpression(AssignmentExpression assignment) 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 @@ public override void VisitAssignmentExpression(AssignmentExpression assignment) 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 fa06121563..0fecc7cb47 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs @@ -75,6 +75,7 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression) 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 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression) } expr.CopyAnnotationsFrom(invocationExpression); invocationExpression.ReplaceWith(expr); + context.EndStep(expr); return; } @@ -107,9 +109,11 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression) { 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 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression) 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 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression) 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 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression) { 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 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression) // 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 @@ void ProcessInvocationExpression(InvocationExpression invocationExpression) { 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 505587ef88..9b73530ba1 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs @@ -18,10 +18,13 @@ #nullable enable +using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Threading; using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.DebugSteps; using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -36,6 +39,7 @@ public class TransformContext public readonly TypeSystemAstBuilder TypeSystemAstBuilder; public readonly DecompilerSettings Settings; internal readonly DecompileRun DecompileRun; + public Stepper Stepper { get; set; } readonly ITypeResolveContext decompilationContext; @@ -67,6 +71,72 @@ internal TransformContext(IDecompilerTypeSystem typeSystem, DecompileRun decompi 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) + { + Stepper.Step(description, CreateNodeInfo(near)); + } + + [Conditional("STEP")] + [DebuggerStepThrough] + internal void StepStartGroup(string description, AstNode? near = null) + { + Stepper.StartGroup(description, CreateNodeInfo(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 && modifiedNode != null) + { + var marker = new DebugStepMarker(); + modifiedNode.AddAnnotation(marker); + Stepper.LastStep.ModifiedNode = modifiedNode; + Stepper.LastStep.RecordModifiedNode(modifiedNode, extraIdentity: marker, insertFirst: true); + } + } + + static DebugStepNodeInfo? CreateNodeInfo(AstNode? modifiedNode) + { + if (modifiedNode == null) + return null; + // The marker rides CopyAnnotationsFrom so a replaced node's step still resolves to the + // emitted text; it is recorded as a second identity for the changed node. The seam + // neighbors and ancestor chain are captured from the original node before mutation. + var marker = new DebugStepMarker(); + modifiedNode.AddAnnotation(marker); + return new DebugStepNodeInfo( + modifiedNode, + modifiedNode.NextSibling, + modifiedNode.PrevSibling, + Ancestors(modifiedNode), + extraIdentity: marker); + + static IEnumerable Ancestors(AstNode node) + { + for (var parent = node.Parent; parent != null; parent = parent.Parent) + yield return parent; + } } } + } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs index 404109102a..d817decd36 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs @@ -481,6 +481,7 @@ public bool MoveConstructorInitializer(ConstructorDeclaration constructorDeclara 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 @@ public bool MoveConstructorInitializer(ConstructorDeclaration constructorDeclara // Remove the statement stmt.Remove(); + context.EndStep(constructorDeclaration.Initializer); return true; } @@ -507,7 +509,10 @@ public bool MoveFieldInitializersToDeclarations(InitializerSequence sequence, In // 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 @@ public bool MoveFieldInitializersToDeclarations(InitializerSequence sequence, In 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 @@ public bool MoveFieldInitializersToDeclarations(InitializerSequence sequence, In 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 @@ public bool MoveFieldInitializersToDeclarations(InitializerSequence sequence, In 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 @@ public bool MoveFieldInitializersToDeclarations(InitializerSequence sequence, In if (sequence.IsUnsafe && IntroduceUnsafeModifier.IsUnsafe(initializer)) { + context.Step("Add unsafe modifier to initialized member", declaringSyntaxNode); declaringSyntaxNode.Modifiers |= Modifiers.Unsafe; } } @@ -626,6 +641,7 @@ public void RemoveImplicitConstructor() 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 @@ public void RemoveImplicitConstructor() 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 @@ public void RemoveImplicitConstructor() 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 @@ public void RemoveImplicitConstructor() if (IsBeforeFieldInit && StaticConstructorDecl.Body is { Statements.Count: 0 }) { + context.Step("Remove empty static constructor", StaticConstructorDecl); StaticConstructorDecl.Remove(); } } @@ -733,7 +754,10 @@ public void RemoveImplicitConstructor() 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/DebugSteps/Stepper.cs b/ICSharpCode.Decompiler/DebugSteps/Stepper.cs new file mode 100644 index 0000000000..2dab0f0373 --- /dev/null +++ b/ICSharpCode.Decompiler/DebugSteps/Stepper.cs @@ -0,0 +1,281 @@ +// Copyright (c) 2016 Daniel Grunwald +// +// 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +using ICSharpCode.Decompiler.Util; + +namespace ICSharpCode.Decompiler.DebugSteps +{ + /// + /// Exception thrown when a debug-step-enabled transform pipeline runs into the . + /// + public class StepLimitReachedException : Exception + { + } + + /// + /// Language-specific node identities and navigation data captured before a transform mutation. + /// + public readonly struct DebugStepNodeInfo + { + public object Node { get; } + public object? NextSibling { get; } + public object? PreviousSibling { get; } + public IEnumerable? Ancestors { get; } + public object? ExtraIdentity { get; } + + public DebugStepNodeInfo( + object node, + object? nextSibling = null, + object? previousSibling = null, + IEnumerable? ancestors = null, + object? extraIdentity = null) + { + Node = node ?? throw new ArgumentNullException(nameof(node)); + NextSibling = nextSibling; + PreviousSibling = previousSibling; + Ancestors = ancestors; + ExtraIdentity = extraIdentity; + } + } + + /// + /// Helper class that manages recording transform steps. + /// + public class Stepper + { + /// + /// Gets whether stepping of built-in transforms is supported in this build of ICSharpCode.Decompiler. + /// Usually only debug builds support transform stepping. + /// + public static bool SteppingAvailable { + get { +#if STEP + return true; +#else + return false; +#endif + } + } + + 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; } + + public class Node + { + public string Description { get; } + public object? Position { get; set; } + public object? ModifiedNode { get; set; } + /// + /// Precise identities of the changed node (the node itself, its debug-step marker, and + /// any node its mutation produced). Resolved first when highlighting the step. + /// + public IList ModifiedNodeCandidates { get; } = new List(); + /// + /// Neighbors of the changed node, recorded before the mutation. When the node itself is + /// gone from the rendered text (a removal), a caret is placed at the gap they leave: + /// at the start of a following neighbor, or the end of a preceding one (AtEnd). + /// + public IList<(object Anchor, bool AtEnd)> SeamAnchors { get; } = new List<(object, bool)>(); + /// + /// The changed node's ancestor chain, resolved as a last resort when neither the node + /// nor a seam neighbor has a rendered range. + /// + public IList AncestorCandidates { get; } = new List(); + /// + /// BeginStep is inclusive. + /// + public int BeginStep { get; set; } + /// + /// EndStep is exclusive. + /// + public int EndStep { get; set; } + + public IList Children { get; } = new List(); + + public Node(string description) + { + Description = description; + } + + /// + /// Records the highlight candidates for this step from an already-navigated node: the + /// node's identity (optionally a second identity such as a debug-step marker), its + /// immediate siblings as seam anchors, and its ancestor chain. Callers pass the + /// neighbors because each transform representation exposes navigation differently; + /// the ordering/dedup/seam strategy lives here so languages' + /// recording can't drift. marks a produced-node update + /// (from EndStep): the node is preferred over existing candidates and the seam and + /// ancestor anchors are left untouched, since they were captured from the original node. + /// + public void RecordModifiedNode(DebugStepNodeInfo modifiedNode, bool insertFirst = false) + { + RecordModifiedNode( + modifiedNode.Node, + modifiedNode.NextSibling, + modifiedNode.PreviousSibling, + modifiedNode.Ancestors, + modifiedNode.ExtraIdentity, + insertFirst); + } + + public void RecordModifiedNode( + object node, + object? nextSibling = null, + object? previousSibling = null, + IEnumerable? ancestors = null, + object? extraIdentity = null, + bool insertFirst = false) + { + AddCandidate(node, insertFirst); + if (extraIdentity != null) + AddCandidate(extraIdentity, insertFirst: false); + if (insertFirst) + return; + if (nextSibling != null) + SeamAnchors.Add((nextSibling, false)); + if (previousSibling != null) + SeamAnchors.Add((previousSibling, true)); + if (ancestors != null) + { + foreach (var ancestor in ancestors) + AncestorCandidates.Add(ancestor); + } + } + + void AddCandidate(object candidate, bool insertFirst) + { + if (ModifiedNodeCandidates.Contains(candidate)) + return; + if (insertFirst) + ModifiedNodeCandidates.Insert(0, candidate); + else + ModifiedNodeCandidates.Add(candidate); + } + } + + readonly Stack groups; + readonly IList steps; + int step = 0; + + public Stepper() + { + steps = new List(); + groups = new Stack(); + } + + /// + /// Call this method immediately before performing a transform step. + /// Used for debugging the IL transforms. Has no effect in release mode. + /// + /// May throw in debug mode. + /// + [DebuggerStepThrough] + public Node Step(string description, DebugStepNodeInfo? modifiedNode = null) + { + object? node = modifiedNode?.Node; + var stepNode = new Node($"{step}: {description}") { + Position = node, + ModifiedNode = node, + BeginStep = step, + EndStep = step + 1 + }; + // Record highlight candidates before the limit-reached check below can throw: the debug-step + // view halts the pipeline at the selected step, so that step would otherwise carry no + // candidates and the "show state before" view could not locate the change. + if (modifiedNode is { } info) + { + stepNode.RecordModifiedNode(info); + } + if (step == StepLimit) + { + LimitReachedStep = stepNode; + if (IsDebug) + Debugger.Break(); + else + throw new StepLimitReachedException(); + } + var p = groups.PeekOrDefault(); + if (p != null) + p.Children.Add(stepNode); + else + steps.Add(stepNode); + LastStep = stepNode; + step++; + return stepNode; + } + + [DebuggerStepThrough] + public Node StartGroup(string description, DebugStepNodeInfo? modifiedNode = null) + { + var stepNode = Step(description, modifiedNode); + groups.Push(stepNode); + return stepNode; + } + + public Node? GetStepByBeginStep(int beginStep) + { + 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) + { + var node = groups.Pop(); + if (!keepIfEmpty && node.Children.Count == 0) + { + var col = groups.PeekOrDefault()?.Children ?? steps; + Debug.Assert(col.Last() == node); + col.RemoveAt(col.Count - 1); + } + node.EndStep = step; + } + } + + /// + /// Annotation attached to a step's modified node so the debug-step highlighter can still locate + /// that node's rendered range after a later transform copies its annotations onto a replacement + /// (via CopyAnnotationsFrom). This is the only annotation the UI bridges to a text range; indexing + /// every annotation would add dead keys and let shared ones collide by identity. + /// + public sealed class DebugStepMarker + { + } +} diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index 490bc19db0..314f6f1988 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -370,7 +370,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -466,7 +466,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -624,7 +624,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -1009,7 +1009,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayWriteLocals; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -2339,7 +2339,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayReadLocals; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -2413,7 +2413,7 @@ protected override void Disconnected() base.Disconnected(); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -2548,7 +2548,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayWriteLocals; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -2653,7 +2653,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -2818,7 +2818,7 @@ public LdStr(string value) : base(OpCode.LdStr) } public readonly string Value; public override StackType ResultType { get { return StackType.O; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -2855,7 +2855,7 @@ public LdStrUtf8(string value) : base(OpCode.LdStrUtf8) } public readonly string Value; public override StackType ResultType { get { return StackType.O; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -2892,7 +2892,7 @@ public LdcI4(int value) : base(OpCode.LdcI4) } public readonly int Value; public override StackType ResultType { get { return StackType.I4; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -2929,7 +2929,7 @@ public LdcI8(long value) : base(OpCode.LdcI8) } public readonly long Value; public override StackType ResultType { get { return StackType.I8; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -2966,7 +2966,7 @@ public LdcF4(float value) : base(OpCode.LdcF4) } public readonly float Value; public override StackType ResultType { get { return StackType.F4; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3003,7 +3003,7 @@ public LdcF8(double value) : base(OpCode.LdcF8) } public readonly double Value; public override StackType ResultType { get { return StackType.F8; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3040,7 +3040,7 @@ public LdcDecimal(decimal value) : base(OpCode.LdcDecimal) } public readonly decimal Value; public override StackType ResultType { get { return StackType.O; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3107,7 +3107,7 @@ public LdFtn(IMethod method) : base(OpCode.LdFtn) /// Returns the method operand. public IMethod Method => method; public override StackType ResultType { get { return StackType.I; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3158,7 +3158,7 @@ public override InstructionFlags DirectFlags { return base.DirectFlags | InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3219,7 +3219,7 @@ public override InstructionFlags DirectFlags { return base.DirectFlags | InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3269,7 +3269,7 @@ public IType Type { set { type = value; InvalidateFlags(); } } public override StackType ResultType { get { return StackType.O; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3308,7 +3308,7 @@ public LdMemberToken(IMember member) : base(OpCode.LdMemberToken) /// Returns the token operand. public IMember Member { get { return member; } } public override StackType ResultType { get { return StackType.O; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3396,7 +3396,7 @@ public override InstructionFlags DirectFlags { return base.DirectFlags | InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3534,7 +3534,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayThrow | InstructionFlags.SideEffect; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); if (IsVolatile) @@ -3685,7 +3685,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayThrow | InstructionFlags.SideEffect; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); if (IsVolatile) @@ -3801,7 +3801,7 @@ public override InstructionFlags DirectFlags { return (DelayExceptions ? InstructionFlags.None : InstructionFlags.MayThrow); } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); if (DelayExceptions) @@ -3845,7 +3845,7 @@ public LdsFlda(IField @field) : base(OpCode.LdsFlda) readonly IField @field; /// Returns the field operand. public IField Field { get { return @field; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3896,7 +3896,7 @@ public override InstructionFlags DirectFlags { return base.DirectFlags | InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -3941,7 +3941,7 @@ public IType Type { set { type = value; InvalidateFlags(); } } public override StackType ResultType { get { return StackType.O; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -4164,7 +4164,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.SideEffect | InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -4355,7 +4355,7 @@ public IType Type { set { type = value; InvalidateFlags(); } } public override StackType ResultType { get { return StackType.O; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -4409,7 +4409,7 @@ public override InstructionFlags DirectFlags { return base.DirectFlags | InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -4463,7 +4463,7 @@ public override InstructionFlags DirectFlags { return base.DirectFlags | InstructionFlags.SideEffect | InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -4580,7 +4580,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -4633,7 +4633,7 @@ public IType Type { set { type = value; InvalidateFlags(); } } public override StackType ResultType { get { return type.GetStackType(); } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -4749,7 +4749,7 @@ public IType Type { set { type = value; InvalidateFlags(); } } public override StackType ResultType { get { return StackType.I4; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -4950,7 +4950,7 @@ public override InstructionFlags DirectFlags { return (DelayExceptions ? InstructionFlags.None : InstructionFlags.MayThrow); } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); if (WithSystemIndex) @@ -5075,7 +5075,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); if (IsReadOnly) @@ -5188,7 +5188,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -5436,7 +5436,7 @@ public sealed override ILInstruction Clone() clone.Right = this.right.Clone(); return clone; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -6633,7 +6633,7 @@ public IType Type { set { type = value; InvalidateFlags(); } } public override StackType ResultType { get { return StackType.O; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -6715,7 +6715,7 @@ public override InstructionFlags DirectFlags { return base.DirectFlags | InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -6813,7 +6813,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayBranch | InstructionFlags.SideEffect; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -6909,7 +6909,7 @@ public override InstructionFlags DirectFlags { return InstructionFlags.SideEffect; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -7029,7 +7029,7 @@ public sealed override ILInstruction Clone() var clone = (AnyNode)ShallowClone(); return clone; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index 74963251be..96fcb35ef5 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -427,10 +427,11 @@ namespace <#=opCode.Namespace#> <# if (opCode.GenerateWriteTo) { #> <# if (opCode.CustomWriteToButKeepOriginal) { #> void OriginalWriteTo(ITextOutput output, ILAstWritingOptions options) + {<#=Body(opCode.WriteToBody)#>} <# } else { #> - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) -<# } #> + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) {<#=Body(opCode.WriteToBody)#>} +<# } #> <# } #> <# if (opCode.GenerateAcceptVisitor) { #> public override void AcceptVisitor(ILVisitor visitor) diff --git a/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs index ba88de7b7d..b3499fb9ae 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs @@ -184,7 +184,7 @@ internal static string GetOperatorName(BinaryNumericOperator @operator) } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/Block.cs b/ICSharpCode.Decompiler/IL/Instructions/Block.cs index 2b0b3991e6..6c8206c46b 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Block.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Block.cs @@ -258,7 +258,7 @@ public string Label { get { return Disassembler.DisassemblerHelpers.OffsetToString(this.StartILOffset); } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write("Block "); diff --git a/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs b/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs index 131bf5f3b4..9c212982d5 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs @@ -125,7 +125,7 @@ protected override void Disconnected() entryPoint.IncomingEdgeCount--; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.WriteLocalReference("BlockContainer", this, isDefinition: true); diff --git a/ICSharpCode.Decompiler/IL/Instructions/Branch.cs b/ICSharpCode.Decompiler/IL/Instructions/Branch.cs index bab8b9fc06..9f5aee7455 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Branch.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Branch.cs @@ -118,7 +118,7 @@ internal override void CheckInvariant(ILPhase phase) } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs b/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs index e165f5df02..54a2b88805 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs @@ -74,7 +74,7 @@ internal override void CheckInvariant(ILPhase phase) Debug.Assert(Arguments.Count == FunctionPointerType.ParameterTypes.Length + (IsInstance ? 1 : 0)); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write("call.indirect "); diff --git a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs index a09aa66212..3aa46e16e7 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs @@ -138,7 +138,7 @@ internal override void CheckInvariant(ILPhase phase) } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); if (ConstrainedTo != null) diff --git a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs index c7e26ff459..2a67566443 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs @@ -179,7 +179,7 @@ internal override void CheckInvariant(ILPhase phase) } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); if (options.UseLogicOperationSugar && MatchLogicNot(out var arg)) diff --git a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs index eab1cbb290..85f49aad6e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs @@ -272,7 +272,7 @@ public override InstructionFlags DirectFlags { } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -336,7 +336,7 @@ public static bool IsStringConcat(IMethod method) public override StackType ResultType => Method.ReturnType.GetStackType(); - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -372,7 +372,7 @@ public DynamicCompoundAssign(ExpressionType op, CSharpBinderFlags binderFlags, this.ValueArgumentInfo = valueArgumentInfo; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/Conv.cs b/ICSharpCode.Decompiler/IL/Instructions/Conv.cs index e2ae200c9a..e029b18947 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Conv.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Conv.cs @@ -314,7 +314,7 @@ public StackType UnderlyingResultType { get => TargetType.GetStackType(); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs index 4a4b0e1b2e..f2993a7f84 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs @@ -159,7 +159,7 @@ protected internal override void InstructionCollectionUpdateComplete() assignments.ChildIndex = Init.Count + 2; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write("deconstruct "); diff --git a/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs index 358b761efa..608aa4483c 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs @@ -37,7 +37,7 @@ public DeconstructResultInstruction(int index, StackType resultType, ILInstructi ResultType = resultType; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs b/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs index ee878ebb48..3e037a336e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs @@ -130,7 +130,7 @@ internal static void WriteArgumentList(ITextOutput output, ILAstWritingOptions o partial class DynamicConvertInstruction { - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -182,7 +182,7 @@ public DynamicInvokeMemberInstruction(CSharpBinderFlags binderFlags, string name Arguments.AddRange(arguments); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -228,7 +228,7 @@ public DynamicGetMemberInstruction(CSharpBinderFlags binderFlags, string? name, Target = target; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -264,7 +264,7 @@ public DynamicSetMemberInstruction(CSharpBinderFlags binderFlags, string? name, Value = value; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -302,7 +302,7 @@ public DynamicGetIndexInstruction(CSharpBinderFlags binderFlags, IType? context, Arguments.AddRange(arguments); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -334,7 +334,7 @@ public DynamicSetIndexInstruction(CSharpBinderFlags binderFlags, IType? context, Arguments.AddRange(arguments); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -369,7 +369,7 @@ public DynamicInvokeConstructorInstruction(CSharpBinderFlags binderFlags, IType? this.resultType = type; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -406,7 +406,7 @@ public DynamicBinaryOperatorInstruction(CSharpBinderFlags binderFlags, Expressio Right = right; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -448,7 +448,7 @@ public DynamicLogicOperatorInstruction(CSharpBinderFlags binderFlags, Expression Right = right; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -495,7 +495,7 @@ public DynamicUnaryOperatorInstruction(CSharpBinderFlags binderFlags, Expression Operand = operand; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -542,7 +542,7 @@ public DynamicInvokeInstruction(CSharpBinderFlags binderFlags, IType? context, C Arguments.AddRange(arguments); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -572,7 +572,7 @@ public DynamicIsEventInstruction(CSharpBinderFlags binderFlags, string? name, IT Argument = argument; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs b/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs index b3dc073683..670ebe6531 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs @@ -14,7 +14,7 @@ public ExpressionTreeCast(IType type, ILInstruction argument, bool isChecked) this.IsChecked = isChecked; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs b/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs index eb81ccbcea..762d57e8a6 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs @@ -270,7 +270,7 @@ void CloneVariables() throw new NotSupportedException("ILFunction.CloneVariables is currently not supported!"); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs index 028294cd54..e362d8a293 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ILInstruction.cs @@ -383,9 +383,29 @@ public void WriteILRange(ITextOutput output, ILAstWritingOptions options) } /// - /// Writes the ILAst to the text output. + /// Writes the ILAst to the text output. Brackets the instruction's rendered span so it can be + /// mapped back to its character range for debug-step highlighting; the bracket lives here, in + /// one place, so a new instruction cannot forget it. Override to + /// render an instruction, not this method. Both marks are no-ops unless the output tracks nodes. /// - public abstract void WriteTo(ITextOutput output, ILAstWritingOptions options); + public void WriteTo(ITextOutput output, ILAstWritingOptions options) + { + output.MarkNodeStart(this); + try + { + WriteToCore(output, options); + } + finally + { + output.MarkNodeEnd(this); + } + } + + /// + /// Renders this instruction to the text output. This is the override point; callers use + /// , which wraps this in the node-tracking bracket. + /// + protected abstract void WriteToCore(ITextOutput output, ILAstWritingOptions options); public override string ToString() { diff --git a/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs index 3cd3caf931..f61e8b510c 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs @@ -82,7 +82,7 @@ protected override InstructionFlags ComputeFlags() return InstructionFlags.ControlFlow | condition.Flags | SemanticHelper.CombineBranches(trueInst.Flags, falseInst.Flags); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); if (options.UseLogicOperationSugar) diff --git a/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs b/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs index 844552b2cf..7a3603d5c8 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs @@ -39,7 +39,7 @@ public override StackType ResultType { get { return resultType; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/Leave.cs b/ICSharpCode.Decompiler/IL/Instructions/Leave.cs index bb1bc7de30..4d231e9b38 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Leave.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Leave.cs @@ -114,7 +114,7 @@ internal override void CheckInvariant(ILPhase phase) Debug.Assert(phase <= ILPhase.InILReader || phase == ILPhase.InAsyncAwait || value.ResultType == targetContainer!.ResultType); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs index deb89f0a3f..13bfe9afc3 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs @@ -27,7 +27,7 @@ namespace ICSharpCode.Decompiler.IL { partial class LockInstruction { - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write("lock ("); diff --git a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs index 6939764afa..899b7de9aa 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs @@ -274,7 +274,7 @@ internal static bool IsDeconstructMethod(IMethod? method) return true; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs b/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs index a8e333344d..fe4d687f17 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs @@ -37,7 +37,7 @@ interface ISupportsVolatilePrefix partial class LdObj { - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { if (options.UseFieldSugar) { @@ -65,7 +65,7 @@ public override void WriteTo(ITextOutput output, ILAstWritingOptions options) partial class StObj { - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { if (options.UseFieldSugar) { diff --git a/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs index 0d6ad6cbed..67085e0406 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs @@ -90,7 +90,7 @@ protected override InstructionFlags ComputeFlags() | SemanticHelper.CombineBranches(InstructionFlags.None, fallbackInst.Flags); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs b/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs index b1726c43a7..8fc20f6a3a 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs @@ -90,7 +90,7 @@ internal override void CheckInvariant(ILPhase phase) Debug.Assert(Ancestors.Any(a => a is NullableRewrap)); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { output.Write("nullable.unwrap."); if (RefInput) diff --git a/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs index d5d83ab1ac..661c999fae 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs @@ -24,7 +24,7 @@ namespace ICSharpCode.Decompiler.IL /// public abstract partial class SimpleInstruction : ILInstruction { - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -44,7 +44,7 @@ partial class Nop public NopKind Kind; - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -73,7 +73,7 @@ public override StackType ResultType { get { return ExpectedResultType; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); @@ -101,7 +101,7 @@ public override StackType ResultType { get { return ExpectedResultType; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs b/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs index cdb9bee8a6..c095665f75 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs @@ -52,7 +52,7 @@ public StringToInt(ILInstruction argument, string?[] map, IType expectedType) return dict; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write("string.to.int "); diff --git a/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs index 8800626202..7f4874a7d5 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs @@ -83,7 +83,7 @@ public override InstructionFlags DirectFlags { } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write("switch"); @@ -224,7 +224,7 @@ public override InstructionFlags DirectFlags { } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); if (IsCompilerGeneratedDefaultSection) diff --git a/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs index f8a7da9935..048a178f06 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs @@ -68,7 +68,7 @@ public override ILInstruction Clone() return clone; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(".try "); @@ -164,7 +164,7 @@ public override InstructionFlags DirectFlags { } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write("catch "); @@ -217,7 +217,7 @@ public override ILInstruction Clone() return new TryFinally(TryBlock.Clone(), finallyBlock.Clone()).WithILRange(this); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(".try "); @@ -314,7 +314,7 @@ public override ILInstruction Clone() return new TryFault(TryBlock.Clone(), faultBlock.Clone()).WithILRange(this); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(".try "); diff --git a/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs index 2ae492af85..b4bf7bdc6b 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs @@ -50,7 +50,7 @@ internal override void CheckInvariant(ILPhase phase) Debug.Assert(IsLifted || ResultType == UnderlyingResultType); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write(OpCode); diff --git a/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs index f082457ffc..4ea93aacd2 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs @@ -40,7 +40,7 @@ partial class UsingInstruction public bool IsRefStruct { get; set; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); output.Write("using"); diff --git a/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs index 4bfa3a9963..e67f311230 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs @@ -102,6 +102,7 @@ bool CachedDelegateInitializationWithField(IfInstruction inst) return false; context.Step("CachedDelegateInitializationWithField", inst); usages[0].ReplaceWith(value); + context.EndStep(value); return true; } @@ -141,6 +142,7 @@ bool CachedDelegateInitializationWithLocal(IfInstruction inst) context.Step("CachedDelegateInitializationWithLocal", inst); ((Block)otherStore.Parent).Instructions.Remove(otherStore); inst.ReplaceWith(storeInst); + context.EndStep(storeInst); return true; } @@ -247,7 +249,9 @@ bool CachedDelegateInitializationVB(IfInstruction inst) if (!DelegateConstruction.MatchDelegateConstruction(delegateConstruction, out _, out _, out _, true)) return false; context.Step("CachedDelegateInitializationVB", inst); - inst.ReplaceWith(new StLoc(s, delegateConstruction)); + var stloc = new StLoc(s, delegateConstruction); + inst.ReplaceWith(stloc); + context.EndStep(stloc); return true; } @@ -318,7 +322,9 @@ bool CachedDelegateInitializationVBWithClosure(IfInstruction inst) if (!DelegateConstruction.MatchDelegateConstruction(delegateConstruction, out _, out _, out _, true)) return false; context.Step("CachedDelegateInitializationVBWithClosure", inst); - inst.ReplaceWith(new StLoc(s, delegateConstruction)); + var stloc = new StLoc(s, delegateConstruction); + inst.ReplaceWith(stloc); + context.EndStep(stloc); return true; } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs b/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs index b78139c4c6..f55bee55a5 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs @@ -85,6 +85,7 @@ static void RunOnBlock(Block block, ILTransformContext context, HashSet comp(...) context.Step("Remove redundant comp(... != 0)", inst); inst.Left.AddILRange(inst); - inst.ReplaceWith(inst.Left); - inst.Left.AcceptVisitor(this); + var left = inst.Left; + inst.ReplaceWith(left); + context.EndStep(left); + left.AcceptVisitor(this); return; } if (context.Settings.LiftNullables) @@ -171,7 +173,9 @@ protected internal override void VisitConv(Conv inst) { context.Step("conv.i4(ldlen array) => ldlen.i4(array)", inst); inst.AddILRange(inst.Argument); - inst.ReplaceWith(new LdLen(inst.TargetType.GetStackType(), array).WithILRange(inst)); + var ldLen = new LdLen(inst.TargetType.GetStackType(), array).WithILRange(inst); + inst.ReplaceWith(ldLen); + context.EndStep(ldLen); return; } if (inst.TargetType.IsFloatType() && inst.Argument is Conv conv @@ -181,7 +185,9 @@ protected internal override void VisitConv(Conv inst) // so the C# compiler usually follows it with an explicit conv.r4 or conv.r8. // To avoid emitting '(float)(double)val', we combine these two conversions: context.Step("conv.rN(conv.r.un(...)) => conv.rN.un(...)", inst); - inst.ReplaceWith(new Conv(conv.Argument, conv.InputType, conv.InputSign, inst.TargetType, inst.CheckForOverflow, inst.IsLifted | conv.IsLifted)); + var newConv = new Conv(conv.Argument, conv.InputType, conv.InputSign, inst.TargetType, inst.CheckForOverflow, inst.IsLifted | conv.IsLifted); + inst.ReplaceWith(newConv); + context.EndStep(newConv); return; } } @@ -194,7 +200,9 @@ protected internal override void VisitBox(Box inst) // For reference types, box is a no-op. context.Step("box ref-type(arg) => arg", inst); inst.Argument.AddILRange(inst); - inst.ReplaceWith(inst.Argument); + var arg = inst.Argument; + inst.ReplaceWith(arg); + context.EndStep(arg); } } @@ -223,6 +231,7 @@ void CleanUpArrayIndices(InstructionCollection indices) { context.Step("Remove conv.i from array index", index); index.ReplaceWith(conv.Argument); + context.EndStep(conv.Argument); } } } @@ -238,6 +247,7 @@ void VisitLogicNot(Comp inst, ILInstruction arg) comp.Kind = comp.Kind.Negate(); comp.AddILRange(inst); inst.ReplaceWith(comp); + context.EndStep(comp); } comp.AcceptVisitor(this); } @@ -253,6 +263,7 @@ void VisitLogicNot(Comp inst, ILInstruction arg) ifInst.TrueInst = new LdcI4(1).WithILRange(ldc0); ifInst.FalseInst = Comp.LogicNot(rhs).WithILRange(inst); inst.ReplaceWith(ifInst); + context.EndStep(ifInst); ifInst.AcceptVisitor(this); } else if (arg.MatchLogicOr(out lhs, out rhs)) @@ -267,6 +278,7 @@ void VisitLogicNot(Comp inst, ILInstruction arg) ifInst.TrueInst = Comp.LogicNot(rhs).WithILRange(inst); ifInst.FalseInst = new LdcI4(0).WithILRange(ldc1); inst.ReplaceWith(ifInst); + context.EndStep(ifInst); ifInst.AcceptVisitor(this); } else @@ -286,6 +298,7 @@ protected internal override void VisitCall(Call inst) UnderlyingResultType = fallback.ResultType }; inst.ReplaceWith(replacement.WithILRange(inst)); + context.EndStep(replacement); replacement.AcceptVisitor(this); return; } @@ -293,6 +306,7 @@ protected internal override void VisitCall(Call inst) { context.Step("TransformRuntimeHelpersCreateSpanInitialization: single-dim", inst); inst.ReplaceWith(replacement2); + context.EndStep(replacement2); replacement2.AcceptVisitor(this); return; } @@ -316,6 +330,7 @@ protected internal override void VisitNewObj(NewObj inst) { context.Step("new Span(stackalloc) -> stackalloc Span", inst); inst.ReplaceWith(locallocSpan); + context.EndStep(locallocSpan); ILInstruction stmt = Block.GetContainingStatement(locallocSpan); // Special case to eliminate extra store if (stmt.GetNextSibling() is StLoc storeStmt && storeStmt.Value is LdLoc) @@ -326,12 +341,14 @@ protected internal override void VisitNewObj(NewObj inst) { context.Step("TransformSpanTArrayInitialization: single-dim", inst); inst.ReplaceWith(replacement); + context.EndStep(replacement); return; } if (TransformDelegateCtorLdVirtFtnToLdVirtDelegate(inst, out LdVirtDelegate ldVirtDelegate)) { context.Step("new Delegate(target, ldvirtftn Method) -> ldvirtdelegate Delegate Method(target)", inst); inst.ReplaceWith(ldVirtDelegate); + context.EndStep(ldVirtDelegate); return; } base.VisitNewObj(inst); @@ -465,6 +482,7 @@ protected internal override void VisitLdObj(LdObj inst) { context.Step("TransformDecimalFieldToConstant", inst); inst.ReplaceWith(decimalConstant); + context.EndStep(decimalConstant); return; } } @@ -476,7 +494,9 @@ protected internal override void VisitLdObjIfRef(LdObjIfRef inst) { context.Step("ldobj.if.ref(addressof(...)) -> addressof(...)", inst); // there already is a temporary, so the ldobj.if.ref is a no-op in both cases - inst.ReplaceWith(inst.Target); + var target = inst.Target; + inst.ReplaceWith(target); + context.EndStep(target); return; } if (inst.Target.MatchLdLoc(out var s) && s.IsSingleDefinition && s.LoadCount == 1 @@ -486,7 +506,9 @@ protected internal override void VisitLdObjIfRef(LdObjIfRef inst) { context.Step("Single use of ldobj.if.ref(ldloc v) -> ldloc v", inst); // there already is a temporary, so the ldobj.if.ref is a no-op in both cases - inst.ReplaceWith(inst.Target); + var target = inst.Target; + inst.ReplaceWith(target); + context.EndStep(target); return; } } @@ -551,6 +573,7 @@ protected internal override void VisitIfInstruction(IfInstruction inst) context.Step("User-defined short-circuiting logic operator (roslyn pattern)", condition); transformed.AddILRange(inst); inst.ReplaceWith(transformed); + context.EndStep(transformed); return; } } @@ -559,7 +582,9 @@ protected internal override void VisitIfInstruction(IfInstruction inst) { context.Step("match(x) ? true : false -> match(x)", inst); inst.Condition.AddILRange(inst); - inst.ReplaceWith(inst.Condition); + var matchCondition = inst.Condition; + inst.ReplaceWith(matchCondition); + context.EndStep(matchCondition); return; } } @@ -580,7 +605,9 @@ IfInstruction HandleConditionalOperator(IfInstruction inst) context.Step("conditional operator", inst); var newIf = new IfInstruction(Comp.LogicNot(inst.Condition), value2, value1); newIf.AddILRange(inst); - inst.ReplaceWith(new StLoc(v, newIf)); + var stLoc = new StLoc(v, newIf); + inst.ReplaceWith(stLoc); + context.EndStep(stLoc); context.RequestRerun(); // trigger potential inlining of the newly created StLoc return newIf; } @@ -688,11 +715,15 @@ private void HandleSwitchExpression(BlockContainer container, SwitchInstruction } if (resultVariable != null) { - container.ReplaceWith(new StLoc(resultVariable, switchInst)); + var stLoc = new StLoc(resultVariable, switchInst); + container.ReplaceWith(stLoc); + context.EndStep(stLoc); } else { - container.ReplaceWith(new Leave(leaveTarget, switchInst)); + var leaveInst = new Leave(leaveTarget, switchInst); + container.ReplaceWith(leaveInst); + context.EndStep(leaveInst); } context.RequestRerun(); // new StLoc might trigger inlining } @@ -746,6 +777,7 @@ bool TransformDynamicAddAssignOrRemoveAssign(IfInstruction inst) return false; context.Step("+= / -= dynamic.isevent pattern -> dynamic.compound.op", inst); inst.ReplaceWith(dynamicCompoundAssign); + context.EndStep(dynamicCompoundAssign); return true; } @@ -775,7 +807,9 @@ internal static void TransformDynamicSetMemberInstruction(DynamicSetMemberInstru if (inst.Name != dynamicGetMember.Name || !DynamicCompoundAssign.IsExpressionTypeSupported(binaryOp.Operation)) return; context.Step("dynamic.setmember.compound -> dynamic.compound.op", inst); - inst.ReplaceWith(new DynamicCompoundAssign(binaryOp.Operation, binaryOp.BinderFlags, binaryOp.Left, binaryOp.LeftArgumentInfo, binaryOp.Right, binaryOp.RightArgumentInfo)); + var compoundAssign = new DynamicCompoundAssign(binaryOp.Operation, binaryOp.BinderFlags, binaryOp.Left, binaryOp.LeftArgumentInfo, binaryOp.Right, binaryOp.RightArgumentInfo); + inst.ReplaceWith(compoundAssign); + context.EndStep(compoundAssign); } /// @@ -806,7 +840,9 @@ protected internal override void VisitDynamicSetIndexInstruction(DynamicSetIndex if (!DynamicCompoundAssign.IsExpressionTypeSupported(binaryOp.Operation)) return; context.Step("dynamic.setindex.compound -> dynamic.compound.op", inst); - inst.ReplaceWith(new DynamicCompoundAssign(binaryOp.Operation, binaryOp.BinderFlags, binaryOp.Left, binaryOp.LeftArgumentInfo, binaryOp.Right, binaryOp.RightArgumentInfo)); + var compoundAssign = new DynamicCompoundAssign(binaryOp.Operation, binaryOp.BinderFlags, binaryOp.Left, binaryOp.LeftArgumentInfo, binaryOp.Right, binaryOp.RightArgumentInfo); + inst.ReplaceWith(compoundAssign); + context.EndStep(compoundAssign); } protected internal override void VisitBinaryNumericInstruction(BinaryNumericInstruction inst) @@ -894,7 +930,9 @@ void TransformCatchVariable(TryCatchHandler handler, Block entryPoint, bool isCa if (inlinedUnboxAny.Type.Equals(handler.Variable.Type)) { context.Step("TransformCatchVariable - remove inlined UnboxAny", inlinedUnboxAny); - inlinedUnboxAny.ReplaceWith(inlinedUnboxAny.Argument); + var unboxArgument = inlinedUnboxAny.Argument; + inlinedUnboxAny.ReplaceWith(unboxArgument); + context.EndStep(unboxArgument); foreach (var range in inlinedUnboxAny.ILRanges) handler.AddExceptionSpecifierILRange(range); } @@ -945,6 +983,7 @@ void TransformCatchWhen(TryCatchHandler handler, Block entryPoint) { context.Step("TransformCatchWhen", entryPoint.Instructions[0]); handler.Filter = condition; + context.EndStep(condition); } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/FixRemainingIncrements.cs b/ICSharpCode.Decompiler/IL/Transforms/FixRemainingIncrements.cs index 1e5bfa33b3..5073752a5d 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/FixRemainingIncrements.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/FixRemainingIncrements.cs @@ -65,9 +65,10 @@ void IILTransform.Run(ILFunction function, ILTransformContext context) // stloc V(...) // compound.assign op_Increment(V) call.ReplaceWith(call.Arguments[0]); - block.Instructions.Insert(store.ChildIndex + 1, - new UserDefinedCompoundAssign(call.Method, CompoundEvalMode.EvaluatesToNewValue, - new LdLoca(store.Variable), CompoundTargetKind.Address, new LdcI4(1)).WithILRange(call)); + var compoundAssign = new UserDefinedCompoundAssign(call.Method, CompoundEvalMode.EvaluatesToNewValue, + new LdLoca(store.Variable), CompoundTargetKind.Address, new LdcI4(1)).WithILRange(call); + block.Instructions.Insert(store.ChildIndex + 1, compoundAssign); + context.EndStep(compoundAssign); } else { @@ -80,8 +81,10 @@ void IILTransform.Run(ILFunction function, ILTransformContext context) } newVariable.Type = call.GetParameter(0).Type; Debug.Assert(call.Arguments[0].MatchLdLoc(newVariable)); - call.ReplaceWith(new UserDefinedCompoundAssign(call.Method, CompoundEvalMode.EvaluatesToNewValue, - new LdLoca(newVariable), CompoundTargetKind.Address, new LdcI4(1)).WithILRange(call)); + var compoundAssign = new UserDefinedCompoundAssign(call.Method, CompoundEvalMode.EvaluatesToNewValue, + new LdLoca(newVariable), CompoundTargetKind.Address, new LdcI4(1)).WithILRange(call); + call.ReplaceWith(compoundAssign); + context.EndStep(compoundAssign); } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs index f7044b7042..4c00a88716 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs @@ -19,11 +19,13 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading; using ICSharpCode.Decompiler.CSharp.Resolver; using ICSharpCode.Decompiler.CSharp.TypeSystem; +using ICSharpCode.Decompiler.DebugSteps; using ICSharpCode.Decompiler.DebugInfo; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; @@ -105,14 +107,14 @@ internal ILReader CreateILReader() [DebuggerStepThrough] internal void Step(string description, ILInstruction? near) { - Stepper.Step(description, near); + Stepper.Step(description, CreateNodeInfo(near)); } [Conditional("STEP")] [DebuggerStepThrough] internal void StepStartGroup(string description, ILInstruction? near = null) { - Stepper.StartGroup(description, near); + Stepper.StartGroup(description, CreateNodeInfo(near)); } [Conditional("STEP")] @@ -120,5 +122,45 @@ internal void StepEndGroup(bool keepIfEmpty = false) { Stepper.EndGroup(keepIfEmpty); } + + /// + /// Points the most recently recorded step at the instruction its mutation produced. + /// Call this after a whose modified instruction only comes into + /// existence during the mutation (e.g. the result of a ReplaceWith or a freshly + /// inserted instruction). The step already carries the original position and its + /// ancestors as fallback candidates (see ); this prepends the + /// produced instruction so it is preferred. + /// + [Conditional("STEP")] + internal void EndStep(ILInstruction? modifiedNode) + { + if (Stepper.LastStep is { } step && modifiedNode != null) + { + step.ModifiedNode = modifiedNode; + step.RecordModifiedNode(modifiedNode, insertFirst: true); + } + } + + static DebugStepNodeInfo? CreateNodeInfo(ILInstruction? instruction) + { + if (instruction == null) + return null; + object? next = null, previous = null; + if (instruction.Parent is { } parent) + { + int index = instruction.ChildIndex; + if (index + 1 < parent.Children.Count) + next = parent.Children[index + 1]; + if (index - 1 >= 0) + previous = parent.Children[index - 1]; + } + return new DebugStepNodeInfo(instruction, next, previous, Ancestors(instruction)); + + static IEnumerable Ancestors(ILInstruction instruction) + { + for (var node = instruction.Parent; node != null; node = node.Parent) + yield return node; + } + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index 2c60cfc43d..6c0c98c880 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -230,7 +230,9 @@ public static bool InlineOne(StLoc stloc, InliningOptions options, ILTransformCo // Assign the ranges of the stloc instruction: stloc.Value.AddILRange(stloc); // Remove the stloc, but keep the inner expression - stloc.ReplaceWith(stloc.Value); + var keptExpression = stloc.Value; + stloc.ReplaceWith(keptExpression); + context.EndStep(keptExpression); return true; } } @@ -288,17 +290,21 @@ static bool DoInline(ILVariable v, ILInstruction inlinedExpression, ILInstructio // Assign the ranges of the ldloc instruction: inlinedExpression.AddILRange(loadInst); + ILInstruction inlinedResult; if (loadInst.OpCode == OpCode.LdLoca) { // it was an ldloca instruction, so we need to use the pseudo-opcode 'addressof' // to preserve the semantics of the compiler-generated temporary Debug.Assert(((LdLoca)loadInst).Variable == v); - loadInst.ReplaceWith(new AddressOf(inlinedExpression, v.Type)); + inlinedResult = new AddressOf(inlinedExpression, v.Type); + loadInst.ReplaceWith(inlinedResult); } else { + inlinedResult = inlinedExpression; loadInst.ReplaceWith(inlinedExpression); } + context.EndStep(inlinedResult); return true; } return false; diff --git a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs index 00aad059f6..a2fc685011 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IndexRangeTransform.cs @@ -269,6 +269,7 @@ void TransformIndexing() newCall.AddILRange(block.Instructions[i]); } call.ReplaceWith(newCall); + context.EndStep(newCall); block.Instructions.RemoveRange(startPos, pos - startPos); } @@ -358,6 +359,7 @@ void TransformSlicing(bool sliceLengthWasMisdetectedAsStartOffset = false) newCall.AddILRange(block.Instructions[i]); } call.ReplaceWith(newCall); + context.EndStep(newCall); block.Instructions.RemoveRange(startPos, pos - startPos); } diff --git a/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs index b79c4dfb21..afcab33bd3 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/InlineArrayTransform.cs @@ -38,7 +38,9 @@ internal static bool RunOnExpression(Call inst, StatementTransformContext contex { context.Step("call get_Item(addressof System.Span{T}(call InlineArrayAsSpan(addr)), index) -> ldelema.inlinearray(addr, index)", inst); } - inst.ReplaceWith(new LdElemaInlineArray(type, addr, index) { IsReadOnly = isReadOnly }.WithILRange(inst)); + var newInst = new LdElemaInlineArray(type, addr, index) { IsReadOnly = isReadOnly }.WithILRange(inst); + inst.ReplaceWith(newInst); + context.EndStep(newInst); return true; } @@ -52,7 +54,9 @@ internal static bool RunOnExpression(Call inst, StatementTransformContext contex { context.Step("call InlineArrayElementRef(addr, index) -> ldelema.inlinearray(addr, index)", inst); } - inst.ReplaceWith(new LdElemaInlineArray(type, addr, index) { IsReadOnly = isReadOnly }.WithILRange(inst)); + var newInst = new LdElemaInlineArray(type, addr, index) { IsReadOnly = isReadOnly }.WithILRange(inst); + inst.ReplaceWith(newInst); + context.EndStep(newInst); return true; } @@ -66,7 +70,9 @@ internal static bool RunOnExpression(Call inst, StatementTransformContext contex { context.Step("call InlineArrayFirstElementRef(addr) -> ldelema.inlinearray(addr, ldc.i4 0)", inst); } - inst.ReplaceWith(new LdElemaInlineArray(type, addr, new LdcI4(0)) { IsReadOnly = isReadOnly }.WithILRange(inst)); + var newInst = new LdElemaInlineArray(type, addr, new LdcI4(0)) { IsReadOnly = isReadOnly }.WithILRange(inst); + inst.ReplaceWith(newInst); + context.EndStep(newInst); return true; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/InterpolatedStringTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/InterpolatedStringTransform.cs index 1d7bab339e..49c26cba06 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/InterpolatedStringTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/InterpolatedStringTransform.cs @@ -74,6 +74,7 @@ void IStatementTransform.Run(Block block, int pos, StatementTransformContext con insertionPoint.ReplaceWith(replacement); replacement.FinalInstruction = callToStringAndClear; block.Instructions.RemoveRange(interpolationStart, interpolationEnd - interpolationStart); + context.EndStep(replacement); } private bool IsKnownCall(Block block, int pos, ILVariable v) diff --git a/ICSharpCode.Decompiler/IL/Transforms/LdLocaDupInitObjTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/LdLocaDupInitObjTransform.cs index cbeea2a2b3..93e26585cb 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/LdLocaDupInitObjTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/LdLocaDupInitObjTransform.cs @@ -69,8 +69,10 @@ private bool TryTransform(Block block, int i, ILTransformContext context) return false; } context.Step("LdLocaDupInitObjTransform", inst1); - block.Instructions[i] = new StLoc(v, inst2.Value).WithILRange(inst2); + var newInst = new StLoc(v, inst2.Value).WithILRange(inst2); + block.Instructions[i] = newInst; block.Instructions[i + 1] = inst1; + context.EndStep(newInst); return true; } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs index 1d93b677a9..763d7e6492 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs @@ -208,13 +208,15 @@ private void TransformUseSites(Dictionary testedVar?.AccessChain IntroduceUnwrap(testedVar, varLoad, mode); - return new NullableRewrap(nonNullInst); + var result = new NullableRewrap(nonNullInst); + context.EndStep(result); + return result; } else if (nullInst.MatchDefaultValue(out var type) && type.IsKnownType(KnownTypeCode.NullableOfT)) { @@ -138,7 +140,9 @@ ILInstruction TryNullPropagation(ILVariable testedVar, ILInstruction nonNullInst // testedVar != null ? testedVar.AccessChain : default(T?) // => testedVar?.AccessChain IntroduceUnwrap(testedVar, varLoad, mode); - return new NullableRewrap(nonNullInst); + var result = new NullableRewrap(nonNullInst); + context.EndStep(result); + return result; } else if (!removedRewrapOrNullableCtor && NullableType.IsNonNullableValueType(returnType) && !returnType.IsByRefLike) @@ -149,13 +153,15 @@ ILInstruction TryNullPropagation(ILVariable testedVar, ILInstruction nonNullInst // (only valid if AccessChain returns a non-nullable value; a by-ref-like type such as // Span is excluded because it cannot be wrapped in Nullable for the ?. / ?? form) IntroduceUnwrap(testedVar, varLoad, mode); - return new NullCoalescingInstruction( + var result = new NullCoalescingInstruction( NullCoalescingKind.NullableWithValueFallback, new NullableRewrap(nonNullInst), nullInst ) { UnderlyingResultType = nullInst.ResultType }; + context.EndStep(result); + return result; } return null; } @@ -205,6 +211,7 @@ internal void RunStatements(Block block, int pos) // has changed, but this part of the code was not properly adjusted. throw new NotSupportedException(); } + context.EndStep(replacement); // Remove the fallback conditions and blocks block.Instructions.RemoveRange(pos + 1, 2); // if the endBlock is only reachable through the current block, @@ -234,9 +241,11 @@ void TryNullPropForVoidCall(ILVariable testedVar, Mode mode, Block body, IfInstr // if (testedVar != null) { testedVar.AccessChain(); } // => testedVar?.AccessChain(); IntroduceUnwrap(testedVar, varLoad, mode); - ifInst.ReplaceWith(new NullableRewrap( + var replacement = new NullableRewrap( bodyInst - ).WithILRange(ifInst)); + ).WithILRange(ifInst); + ifInst.ReplaceWith(replacement); + context.EndStep(replacement); } bool IsValidAccessChain(ILVariable testedVar, Mode mode, ILInstruction inst, out ILInstruction finalLoad) diff --git a/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs index 48fc540770..a688fe5143 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs @@ -60,6 +60,7 @@ public bool Run(IfInstruction ifInst) if (lifted != null) { ifInst.ReplaceWith(lifted); + context.EndStep(lifted); return true; } return false; @@ -80,6 +81,7 @@ public bool Run(BinaryNumericInstruction bni) if (lifted != null) { bni.ReplaceWith(lifted); + context.EndStep(lifted); return true; } return false; @@ -138,6 +140,7 @@ public bool RunStatements(Block block, int pos) { thenLeave.Value = lifted; ifInst.ReplaceWith(thenLeave); + context.EndStep(thenLeave); block.Instructions.Remove(elseLeave); return true; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs index 708ff80c8b..ee17128cb5 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/PatternMatchingTransform.cs @@ -254,6 +254,7 @@ private static ILInstruction DetectPropertySubPatterns(MatchInstruction parentPa condition = Comp.LogicNot(condition); } parentPattern.SubPatterns.Add(condition); + context.EndStep(condition); } else if (PropertyOrFieldAccess(condition, out var target, out _)) { @@ -266,8 +267,10 @@ private static ILInstruction DetectPropertySubPatterns(MatchInstruction parentPa return null; } context.Step("Sub pattern: implicit != 0", condition); - parentPattern.SubPatterns.Add(new Comp(negate ? ComparisonKind.Equality : ComparisonKind.Inequality, - Sign.None, condition, new LdcI4(0))); + var subPattern = new Comp(negate ? ComparisonKind.Equality : ComparisonKind.Inequality, + Sign.None, condition, new LdcI4(0)); + parentPattern.SubPatterns.Add(subPattern); + context.EndStep(subPattern); } else { @@ -299,6 +302,7 @@ private static ILInstruction DetectPropertySubPatterns(MatchInstruction parentPa var varPattern = new MatchInstruction(targetVariable, operand) .WithILRange(block.Instructions[0]); parentPattern.SubPatterns.Add(varPattern); + context.EndStep(varPattern); block.Instructions.RemoveAt(0); targetVariable.Kind = VariableKind.PatternLocal; @@ -382,7 +386,9 @@ private static ILInstruction DetectPropertySubPatterns(MatchInstruction parentPa } context.Step("Nullable.HasValue check -> null pattern", block); - varPattern.ReplaceWith(new Comp(ComparisonKind.Equality, ComparisonLiftingKind.CSharp, StackType.O, Sign.None, varPattern.TestedOperand, new LdNull())); + var nullComp = new Comp(ComparisonKind.Equality, ComparisonLiftingKind.CSharp, StackType.O, Sign.None, varPattern.TestedOperand, new LdNull()); + varPattern.ReplaceWith(nullComp); + context.EndStep(nullComp); block.Instructions.Clear(); block.Instructions.Add(falseInst); return falseInst; @@ -392,7 +398,9 @@ private static ILInstruction DetectPropertySubPatterns(MatchInstruction parentPa if (varPattern.Variable.AddressCount == 1 && context.Settings.PatternCombinators) { context.Step("Nullable.HasValue check -> not null pattern", block); - varPattern.ReplaceWith(new Comp(ComparisonKind.Inequality, ComparisonLiftingKind.CSharp, StackType.O, Sign.None, varPattern.TestedOperand, new LdNull())); + var notNullComp = new Comp(ComparisonKind.Inequality, ComparisonLiftingKind.CSharp, StackType.O, Sign.None, varPattern.TestedOperand, new LdNull()); + varPattern.ReplaceWith(notNullComp); + context.EndStep(notNullComp); block.Instructions.Clear(); block.Instructions.Add(trueInst); return trueInst; @@ -462,6 +470,7 @@ private static ILInstruction DetectPropertySubPatterns(MatchInstruction parentPa comp = Comp.LogicNot(comp); } varPattern.ReplaceWith(comp); + context.EndStep(comp); return trueInst; } else diff --git a/ICSharpCode.Decompiler/IL/Transforms/ProxyCallReplacer.cs b/ICSharpCode.Decompiler/IL/Transforms/ProxyCallReplacer.cs index e2a0c0891f..0672eb5f33 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ProxyCallReplacer.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ProxyCallReplacer.cs @@ -115,6 +115,7 @@ void Run(CallInstruction inst, ILTransformContext context) newInst.AddILRange(inst); newInst.Arguments.ReplaceList(inst.Arguments); inst.ReplaceWith(newInst); + context.EndStep(newInst); } static bool IsDefinedInCurrentOrOuterClass(IMethod method, ITypeDefinition declaringTypeDefinition) diff --git a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs index 2876f141e7..66f431809e 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs @@ -195,7 +195,9 @@ private void ImproveILOrdering(Block block, IfInstruction ifInst, Block continue return; context.Step("Replace leave with keyword exit", ifInst.TrueInst); - block.Instructions.Last().ReplaceWith(keywordExit.Clone()); + var keywordExitClone = keywordExit.Clone(); + block.Instructions.Last().ReplaceWith(keywordExitClone); + context.EndStep(keywordExitClone); } ConditionDetection.InvertIf(block, ifInst, context); @@ -234,7 +236,9 @@ private bool ReduceNesting(Block block, IfInstruction ifInst, ILInstruction exit { Debug.Assert(ifInst.TrueInst is Leave); context.Step("Replace leave with keyword exit", ifInst.TrueInst); - ifInst.TrueInst.ReplaceWith(exitInst.Clone()); + var exitClone = exitInst.Clone(); + ifInst.TrueInst.ReplaceWith(exitClone); + context.EndStep(exitClone); } return true; } @@ -430,7 +434,9 @@ private void EnsureEndPointUnreachable(ILInstruction inst, ILInstruction fallthr if (!block.HasFlag(InstructionFlags.EndPointUnreachable)) { context.Step("Duplicate block exit", fallthroughExit); - block.Instructions.Add(fallthroughExit.Clone()); + var exitClone = fallthroughExit.Clone(); + block.Instructions.Add(exitClone); + context.EndStep(exitClone); } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs b/ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs index 8c73c46f0a..22fd19f0d3 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/RemoveDeadVariableInit.cs @@ -69,7 +69,9 @@ public void Run(ILFunction function, ILTransformContext context) } else { - stloc.ReplaceWith(stloc.Value); + var value = stloc.Value; + stloc.ReplaceWith(value); + context.EndStep(value); } if (stloc.Value is LdLoc ldloc) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs index 65d78d8017..31fe30da2b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs @@ -61,7 +61,9 @@ private bool DoTransform(Block block, ILTransformContext context) if (!MatchBlock2(br.TargetBlock, s, value, out var exitInst)) return false; context.Step("RemoveInfeasiblePath", br); - br.ReplaceWith(exitInst.Clone()); + var newInst = exitInst.Clone(); + br.ReplaceWith(newInst); + context.EndStep(newInst); s.RemoveIfRedundant = true; return true; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/RemoveUnconstrainedGenericReferenceTypeCheck.cs b/ICSharpCode.Decompiler/IL/Transforms/RemoveUnconstrainedGenericReferenceTypeCheck.cs index 99a1d1436d..6069e47c95 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/RemoveUnconstrainedGenericReferenceTypeCheck.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/RemoveUnconstrainedGenericReferenceTypeCheck.cs @@ -140,6 +140,7 @@ void IStatementTransform.Run(Block block, int pos, StatementTransformContext con argument.ReplaceWith(expr); } block.Instructions.RemoveRange(startPos, containingStmt.ChildIndex - startPos); + context.EndStep(containingStmt); } } } \ No newline at end of file diff --git a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs deleted file mode 100644 index abbdddbf1d..0000000000 --- a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2016 Daniel Grunwald -// -// 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. - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -using ICSharpCode.Decompiler.Util; - -namespace ICSharpCode.Decompiler.IL.Transforms -{ - /// - /// Exception thrown when an IL transform runs into the . - /// - public class StepLimitReachedException : Exception - { - } - - /// - /// Helper class that manages recording transform steps. - /// - public class Stepper - { - /// - /// Gets whether stepping of built-in transforms is supported in this build of ICSharpCode.Decompiler. - /// Usually only debug builds support transform stepping. - /// - public static bool SteppingAvailable { - get { -#if STEP - return true; -#else - return false; -#endif - } - } - - public IList Steps => steps; - - public int StepLimit { get; set; } = int.MaxValue; - public bool IsDebug { get; set; } - - public class Node - { - public string Description { get; } - public ILInstruction? Position { get; set; } - /// - /// BeginStep is inclusive. - /// - public int BeginStep { get; set; } - /// - /// EndStep is exclusive. - /// - public int EndStep { get; set; } - - public IList Children { get; } = new List(); - - public Node(string description) - { - Description = description; - } - } - - readonly Stack groups; - readonly IList steps; - int step = 0; - - public Stepper() - { - steps = new List(); - groups = new Stack(); - } - - /// - /// Call this method immediately before performing a transform step. - /// Used for debugging the IL transforms. Has no effect in release mode. - /// - /// May throw in debug mode. - /// - [DebuggerStepThrough] - public void Step(string description, ILInstruction? near = null) - { - StepInternal(description, near); - } - - [DebuggerStepThrough] - private Node StepInternal(string description, ILInstruction? near) - { - if (step == StepLimit) - { - 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); - step++; - return stepNode; - } - - [DebuggerStepThrough] - public void StartGroup(string description, ILInstruction? near = null) - { - groups.Push(StepInternal(description, near)); - } - - public void EndGroup(bool keepIfEmpty = false) - { - var node = groups.Pop(); - if (!keepIfEmpty && node.Children.Count == 0) - { - var col = groups.PeekOrDefault()?.Children ?? steps; - Debug.Assert(col.Last() == node); - col.RemoveAt(col.Count - 1); - } - node.EndStep = step; - } - } -} diff --git a/ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs index 88983bdcce..de0407eb31 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/SwitchOnStringTransform.cs @@ -377,6 +377,7 @@ bool AddSwitchSection(string value, ILInstruction inst) i--; } } + context.EndStep(inst); return true; } @@ -476,6 +477,7 @@ bool SimplifyCSharp1CascadingIfStatements(InstructionCollection i instructions.RemoveAt(i + 1); instructions.RemoveAt(i - 1); + context.EndStep(inst); return true; } @@ -713,6 +715,7 @@ bool MatchLegacySwitchOnStringWithDict(InstructionCollection inst instructions.RemoveRange(i - 1, 2); i -= 2; } + context.EndStep(inst); return true; } @@ -954,6 +957,7 @@ bool MatchLegacySwitchOnStringWithHashtable(Block block, HashtableInitializer ha inst.AddILRange(block.Instructions[i]); block.Instructions[i].ReplaceWith(inst); block.Instructions.RemoveRange(i + 1, 3); + context.EndStep(inst); info.Transformed = true; hashtableInitializers[dictField] = info; return true; @@ -1181,6 +1185,7 @@ SwitchInstruction ReplaceWithSwitchInstruction(int offset) IsCompilerGeneratedDefaultSection = defaultSection.IsCompilerGeneratedDefaultSection }); instructions[offset].ReplaceWith(newSwitch); + context.EndStep(newSwitch); return newSwitch; } } @@ -1325,6 +1330,7 @@ private bool MatchRoslynSwitchOnStringUsingLengthAndChar(Block block, int i) } instructions[i] = newSwitch; instructions.RemoveRange(i + 1, instructions.Count - (i + 1)); + context.EndStep(newSwitch); return true; void InheritCompilerGeneratedDefaultMarker(SwitchInstruction inner) diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs index 9ae14ec994..637919c1c7 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs @@ -69,8 +69,10 @@ bool DoTransform(ILFunction function, Block body, int pos) context.Step("HandleRuntimeHelperInitializeArray: single-dim", inst); var tempStore = context.Function.RegisterVariable(VariableKind.InitializerTarget, v.Type); var block = BlockFromInitializer(tempStore, elementType, arrayLength, values); - body.Instructions[pos] = new StLoc(v, block); + var newStore = new StLoc(v, block); + body.Instructions[pos] = newStore; body.Instructions.RemoveAt(initArrayPos); + context.EndStep(newStore); ILInlining.InlineIfPossible(body, pos, context); return true; } @@ -91,8 +93,10 @@ bool DoTransform(ILFunction function, Block body, int pos) } )); block.FinalInstruction = new LdLoc(tempStore); - body.Instructions[pos] = new StLoc(v, block); + var newStore = new StLoc(v, block); + body.Instructions[pos] = newStore; body.Instructions.RemoveRange(pos + 1, instructionsToRemove); + context.EndStep(newStore); ILInlining.InlineIfPossible(body, pos, context); return true; } @@ -104,8 +108,10 @@ bool DoTransform(ILFunction function, Block body, int pos) block.Instructions.Add(new StLoc(tempStore, new NewArr(elementType, arrayLength.Select(l => new LdcI4(l)).ToArray()))); block.Instructions.AddRange(values.SelectWithIndex((i, value) => StElem(new LdLoc(tempStore), new[] { new LdcI4(i) }, value, elementType))); block.FinalInstruction = new LdLoc(tempStore); - body.Instructions[pos] = new StLoc(finalStore, block); + var newStore = new StLoc(finalStore, block); + body.Instructions[pos] = newStore; body.Instructions.RemoveRange(pos + 1, instructionsToRemove); + context.EndStep(newStore); ILInlining.InlineIfPossible(body, pos, context); return true; } @@ -250,8 +256,10 @@ bool DoTransformMultiDim(ILFunction function, Block body, int pos) { context.Step("HandleRuntimeHelpersInitializeArray: multi-dim", inst); var block = BlockFromInitializer(v, elementType, length, values); - body.Instructions[pos].ReplaceWith(new StLoc(v, block)); + var newStore = new StLoc(v, block); + body.Instructions[pos].ReplaceWith(newStore); body.Instructions.RemoveAt(initArrayPos); + context.EndStep(newStore); ILInlining.InlineIfPossible(body, pos, context); return true; } @@ -270,8 +278,10 @@ bool DoTransformMultiDim(ILFunction function, Block body, int pos) } )); block.FinalInstruction = new LdLoc(tempStore); - body.Instructions[pos] = new StLoc(v, block); + var newStore = new StLoc(v, block); + body.Instructions[pos] = newStore; body.Instructions.RemoveRange(pos + 1, instructionsToRemove); + context.EndStep(newStore); ILInlining.InlineIfPossible(body, pos, context); return true; } @@ -299,8 +309,10 @@ bool DoTransformStackAllocInitializer(Block body, int pos) } block.FinalInstruction = new LdLoc(tempStore); - body.Instructions[pos] = new StLoc(v, block); + var newStore = new StLoc(v, block); + body.Instructions[pos] = newStore; body.Instructions.RemoveAt(pos + 1); + context.EndStep(newStore); ILInlining.InlineIfPossible(body, pos, context); ExpressionTransforms.RunOnSingleStatement(body.Instructions[pos], context); return true; @@ -313,8 +325,10 @@ bool DoTransformStackAllocInitializer(Block body, int pos) block.Instructions.Add(new StLoc(tempStore, locallocExpr)); block.Instructions.AddRange(values.Where(value => value != null).Select(value => RewrapStore(tempStore, value, elementType))); block.FinalInstruction = new LdLoc(tempStore); - body.Instructions[pos] = new StLoc(v, block); + var newStore = new StLoc(v, block); + body.Instructions[pos] = newStore; body.Instructions.RemoveRange(pos + 1, instructionsToRemove); + context.EndStep(newStore); ILInlining.InlineIfPossible(body, pos, context); ExpressionTransforms.RunOnSingleStatement(body.Instructions[pos], context); return true; @@ -911,6 +925,7 @@ bool DoTransformInlineRuntimeHelpersInitializeArray(Block body, int pos) var tempStore = context.Function.RegisterVariable(VariableKind.InitializerTarget, new ArrayType(context.TypeSystem, elementType, arrayLength.Length)); var block = BlockFromInitializer(tempStore, elementType, arrayLength, valuesList.ToArray()); body.Instructions[pos] = block; + context.EndStep(block); ILInlining.InlineIfPossible(body, pos, context); return true; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs index 071ce1797a..8edba814aa 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs @@ -166,7 +166,9 @@ bool TransformInlineAssignmentStObjOrCall(Block block, int pos) block.Instructions.Remove(localStore); block.Instructions.Remove(stobj); stobj.Value = inst.Value; - inst.ReplaceWith(new StLoc(local, stobj)); + var inlineAssignment = new StLoc(local, stobj); + inst.ReplaceWith(inlineAssignment); + context.EndStep(inlineAssignment); // note: our caller will trigger a re-run, which will call HandleStObjCompoundAssign if applicable return true; } @@ -210,7 +212,9 @@ bool TransformInlineAssignmentStObjOrCall(Block block, int pos) Instructions = { call }, FinalInstruction = new LdLoc(newVar) }; - inst.ReplaceWith(new StLoc(local, inlineBlock)); + var inlineAssignment = new StLoc(local, inlineBlock); + inst.ReplaceWith(inlineAssignment); + context.EndStep(inlineAssignment); // because the ExpressionTransforms don't look into inline blocks, manually trigger HandleCallCompoundAssign if (HandleCompoundAssign(call, context)) { @@ -483,6 +487,7 @@ static ExpressionType ToCompound(ExpressionType from) context.RequestRerun(); // moving stloc to top-level might trigger inlining } compoundStore.ReplaceWith(newInst); + context.EndStep(newInst); if (newInst.Parent is Block inlineAssignBlock && inlineAssignBlock.Kind == BlockKind.CallInlineAssign) { // It's possible that we first replaced the instruction in an inline-assign helper block. @@ -541,7 +546,9 @@ bool TransformInlineAssignmentLocal(Block block, int pos) var var = nextInst.Variable; var stackVar = inst.Variable; block.Instructions.RemoveAt(pos); - nextInst.ReplaceWith(new StLoc(stackVar, new StLoc(var, value))); + var inlineAssignment = new StLoc(stackVar, new StLoc(var, value)); + nextInst.ReplaceWith(inlineAssignment); + context.EndStep(inlineAssignment); return true; } @@ -918,6 +925,7 @@ bool TransformPreIncDecOperatorWithInlineStore(Block block, int pos) block.Instructions[pos] = new StLoc(stloc_outer.Variable, new UserDefinedCompoundAssign( operatorCall.Method, CompoundEvalMode.EvaluatesToNewValue, target, targetKind, new LdcI4(1))); } + context.EndStep(block.Instructions[pos]); return true; } @@ -998,6 +1006,7 @@ bool TransformPostIncDecOperatorWithInlineStore(Block block, int pos) block.Instructions[pos] = new StLoc(stloc.Variable, new UserDefinedCompoundAssign( operatorCall.Method, CompoundEvalMode.EvaluatesToOldValue, target, targetKind, new LdcI4(1))); } + context.EndStep(block.Instructions[pos]); return true; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs index 20f563cd6c..4f07d200ee 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs @@ -186,6 +186,7 @@ void IStatementTransform.Run(Block block, int pos, StatementTransformContext con block.Instructions.RemoveRange(pos + 1, initializerItemsCount); siblings[insertionPos] = initializerBlock; ILInlining.InlineIfPossible(block, pos, context); + context.EndStep(initializerBlock); } private static bool TypeContainsInitOnlyProperties(ITypeDefinition? typeDefinition) diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformDisplayClassUsage.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformDisplayClassUsage.cs index 3b9419f10c..d10fe63882 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformDisplayClassUsage.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformDisplayClassUsage.cs @@ -777,7 +777,9 @@ protected internal override void VisitStLoc(StLoc inst) if (inst.Value is StLoc || inst.Value is CompoundAssignmentInstruction) { context.Step($"Remove unused variable assignment {inst.Variable.Name}", inst); - inst.ReplaceWith(inst.Value); + var replacement = inst.Value; + inst.ReplaceWith(replacement); + context.EndStep(replacement); } return; } @@ -787,12 +789,16 @@ protected internal override void VisitStLoc(StLoc inst) if (inst.Value is Block initBlock && initBlock.Kind == BlockKind.ObjectInitializer) { context.Step($"Remove initializer of {inst.Variable.Name}", inst); + ILInstruction firstInlinedStore = null; for (int i = 1; i < initBlock.Instructions.Count; i++) { var stobj = (StObj)initBlock.Instructions[i]; var variable = displayClass.VariablesToDeclare[(IField)((LdFlda)stobj.Target).Field.MemberDefinition]; - parentBlock.Instructions.Insert(inst.ChildIndex + i, new StLoc(variable.GetOrDeclare(), stobj.Value).WithILRange(stobj)); + var inlinedStore = new StLoc(variable.GetOrDeclare(), stobj.Value).WithILRange(stobj); + parentBlock.Instructions.Insert(inst.ChildIndex + i, inlinedStore); + firstInlinedStore ??= inlinedStore; } + context.EndStep(firstInlinedStore); } context.Step($"Remove initializer of {inst.Variable.Name}", inst); parentBlock.Instructions.Remove(inst); @@ -810,10 +816,14 @@ protected internal override void VisitStLoc(StLoc inst) if (referencedDisplayClass != null && displayClasses.TryGetValue(referencedDisplayClass, out _)) { context.Step($"Propagate reference to {referencedDisplayClass.Name} in {inst.Variable}", inst); + ILInstruction firstReplacement = null; foreach (var ld in inst.Variable.LoadInstructions.ToArray()) { - ld.ReplaceWith(new LdLoc(referencedDisplayClass).WithILRange(ld)); + var newLoad = new LdLoc(referencedDisplayClass).WithILRange(ld); + ld.ReplaceWith(newLoad); + firstReplacement ??= newLoad; } + context.EndStep(firstReplacement); parentBlock.Instructions.Remove(inst); return; } @@ -886,7 +896,9 @@ protected internal override void VisitLdFlda(LdFlda inst) var v = displayClass.VariablesToDeclare[keyField]; context.Step($"Replace {field.Name} with captured variable {v.Name}", inst); ILVariable variable = v.GetOrDeclare(); - inst.ReplaceWith(new LdLoca(variable).WithILRange(inst)); + var replacement = new LdLoca(variable).WithILRange(inst); + inst.ReplaceWith(replacement); + context.EndStep(replacement); // add captured variable to all descendant functions from the declaring function to this use-site function foreach (var f in currentFunctions) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs index 1280b3eae7..c196e0c664 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs @@ -135,6 +135,7 @@ bool TryConvertExpressionTree(ILInstruction instruction, ILInstruction statement var newLambda = (ILFunction)lambda(); SetExpressionTreeFlag(newLambda, (CallInstruction)instruction); instruction.ReplaceWith(newLambda); + context.EndStep(newLambda); return true; } return false; diff --git a/ICSharpCode.Decompiler/IL/Transforms/UserDefinedLogicTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/UserDefinedLogicTransform.cs index 1fe5ac8542..7b00a6873d 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/UserDefinedLogicTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/UserDefinedLogicTransform.cs @@ -70,6 +70,7 @@ bool RoslynOptimized(Block block, int pos, StatementTransformContext context) context.Step("User-defined short-circuiting logic operator (optimized return)", condition); ((Leave)block.Instructions[pos + 1]).Value = transformed; block.Instructions.RemoveAt(pos); + context.EndStep(transformed); return true; } } @@ -107,9 +108,11 @@ bool LegacyPattern(Block block, int pos, StatementTransformContext context) if (s.IsUsedWithin(call.Arguments[1])) return false; context.Step("User-defined short-circuiting logic operator (legacy pattern)", condition); - ((StLoc)block.Instructions[pos]).Value = new UserDefinedLogicOperator(call.Method, lhsInst, call.Arguments[1]) + var userLogicOp = new UserDefinedLogicOperator(call.Method, lhsInst, call.Arguments[1]) .WithILRange(call); + ((StLoc)block.Instructions[pos]).Value = userLogicOp; block.Instructions.RemoveAt(pos + 1); + context.EndStep(userLogicOp); context.RequestRerun(); // the 'stloc s' may now be eligible for inlining return true; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs index 26f8cbda26..39c20c13f5 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs @@ -100,9 +100,11 @@ bool TransformUsing(Block block, int i) context.Step("UsingTransform", tryFinally); storeInst.Variable.Kind = VariableKind.UsingLocal; block.Instructions.RemoveAt(i + 1); - block.Instructions[i] = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock) { + var usingInst = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock) { IsRefStruct = context.Settings.IntroduceRefModifiersOnStructs && storeInst.Variable.Type.Kind == TypeKind.Struct && storeInst.Variable.Type.IsByRefLike }.WithILRange(storeInst); + block.Instructions[i] = usingInst; + context.EndStep(usingInst); return true; bool ValidateAddressUse(LdLoca la) @@ -167,7 +169,9 @@ bool TransformUsingVB(Block block, int i) context.Step("UsingTransformVB", tryFinally); storeInst.Variable.Kind = VariableKind.UsingLocal; tryContainer.EntryPoint.Instructions.RemoveAt(0); - block.Instructions[i] = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock); + var usingInst = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock); + block.Instructions[i] = usingInst; + context.EndStep(usingInst); return true; } @@ -500,8 +504,10 @@ private bool TransformAsyncUsing(Block block, int i) context.Step("AsyncUsingTransform", tryFinally); storeInst.Variable.Kind = VariableKind.UsingLocal; block.Instructions.RemoveAt(i); - block.Instructions[i - 1] = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock) { IsAsync = true } + var usingInst = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock) { IsAsync = true } .WithILRange(storeInst); + block.Instructions[i - 1] = usingInst; + context.EndStep(usingInst); return true; } diff --git a/ICSharpCode.Decompiler/Output/INodeTrackingOutput.cs b/ICSharpCode.Decompiler/Output/INodeTrackingOutput.cs new file mode 100644 index 0000000000..79a2e4cd5f --- /dev/null +++ b/ICSharpCode.Decompiler/Output/INodeTrackingOutput.cs @@ -0,0 +1,59 @@ +// 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.Decompiler +{ + /// + /// Optional capability of an that records the rendered text span + /// of written nodes, so a node can later be mapped back to its character range (used for + /// debug-step highlighting). Outputs that don't track nodes simply don't implement this. + /// + public interface INodeTrackingOutput + { + /// Marks the start offset of 's rendered span. + void MarkNodeStart(object node); + + /// + /// Marks the end of 's rendered span; pairs with the most recent + /// for the same node. + /// + void MarkNodeEnd(object node); + } + + public static class NodeTrackingOutputExtensions + { + /// + /// Records the start of 's rendered span when + /// supports node tracking; a cheap no-op otherwise. Safe to call unconditionally from any + /// node-writing code path. + /// + public static void MarkNodeStart(this ITextOutput output, object node) + { + (output as INodeTrackingOutput)?.MarkNodeStart(node); + } + + /// + /// Records the end of 's rendered span when + /// supports node tracking; a cheap no-op otherwise. Must pair with . + /// + public static void MarkNodeEnd(this ITextOutput output, object node) + { + (output as INodeTrackingOutput)?.MarkNodeEnd(node); + } + } +} diff --git a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs index 7cfd12dbe4..7765512241 100644 --- a/ICSharpCode.Decompiler/Output/TextTokenWriter.cs +++ b/ICSharpCode.Decompiler/Output/TextTokenWriter.cs @@ -35,24 +35,20 @@ public class TextTokenWriter : TokenWriter { readonly ITextOutput output; readonly DecompilerSettings settings; - readonly IDecompilerTypeSystem typeSystem; readonly Stack nodeStack = new Stack(); int braceLevelWithinType = -1; bool inDocumentationComment = false; bool firstUsingDeclaration; bool lastUsingDeclaration; - public TextTokenWriter(ITextOutput output, DecompilerSettings settings, IDecompilerTypeSystem typeSystem) + public TextTokenWriter(ITextOutput output, DecompilerSettings settings) { if (output == null) throw new ArgumentNullException(nameof(output)); if (settings == null) throw new ArgumentNullException(nameof(settings)); - if (typeSystem == null) - throw new ArgumentNullException(nameof(typeSystem)); this.output = output; this.settings = settings; - this.typeSystem = typeSystem; } public override void WriteIdentifier(Identifier identifier) diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 6f719e10af..ef49b192b7 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -18,19 +18,28 @@ #if DEBUG +using System; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Headless.NUnit; +using Avalonia.Threading; using Avalonia.VisualTree; using AwesomeAssertions; +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; +using ICSharpCode.Decompiler.DebugSteps; + 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 +128,136 @@ await Waiters.WaitForAsync( "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!; + + await tab.RestartDecompileWithStepLimit(replayStep.BeginStep, isDebug: false, replayStep.BeginStep); + tab.Text.Should().NotBeNullOrWhiteSpace("C# replay before a selected AST mutation step must still emit code"); + AssertPreciseHighlight(tab, "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"); + + await tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep); + tab.Text.Should().NotBeNullOrWhiteSpace("C# replay after a selected AST mutation step must still emit code"); + AssertPreciseHighlight(tab, "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 async Task ILAst_DebugStep_Replay_Highlights_Changed_Instruction() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var languageService = AppComposition.Current.GetExport(); + var blockIL = languageService.Languages.OfType().Single(l => l.Name == "ILAst"); + languageService.CurrentLanguage = blockIL; + + 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 ILAst decompile"); + + // Replaying an individual mutation step is what surfaces a single IL change; the leaf + // step's changed instruction (or a surviving ancestor) must map to a rendered text range. + var replayStep = FirstLeafStep(debugStepsVm.Steps!); + replayStep.Should().NotBeNull("the ILAst stepper must record individual mutation steps"); + + var collectedSteps = debugStepsVm.Steps; + var tab = vm.DockWorkspace.ActiveDecompilerTab!; + + await tab.RestartDecompileWithStepLimit(replayStep!.BeginStep, isDebug: false, replayStep.BeginStep); + tab.Text.Should().NotBeNullOrWhiteSpace("ILAst replay before a selected step must still emit IL"); + AssertPreciseHighlight(tab, "ILAst replay before a selected step must locate the changed instruction"); + debugStepsVm.Steps.Should().BeSameAs(collectedSteps, + "a step-limited ILAst replay must not replace the full step tree shown by the pane"); + + await tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep); + tab.Text.Should().NotBeNullOrWhiteSpace("ILAst replay after a selected step must still emit IL"); + AssertPreciseHighlight(tab, "ILAst replay after a selected step must locate the changed instruction"); + + // The first leaf step that acts on a concrete instruction; a step whose Position is null + // (e.g. an empty transform group) has nothing to highlight and is not what a user replays. + static Stepper.Node? FirstLeafStep(System.Collections.Generic.IEnumerable steps) + { + foreach (var step in steps) + { + if (step.Children.Count == 0) + { + if (step.Position != null) + return step; + continue; + } + var leaf = FirstLeafStep(step.Children); + if (leaf != null) + return leaf; + } + return null; + } + } + [AvaloniaTest] public Task ILAst_And_TypedIL_Languages_Are_Registered_In_Debug_Builds() { @@ -133,6 +272,202 @@ public Task ILAst_And_TypedIL_Languages_Are_Registered_In_Debug_Builds() languageService.Languages.Should().Contain(l => l.Name == "Typed IL"); return Task.CompletedTask; } + + [AvaloniaTest] + public Task NodeLookup_Resolves_Copied_Ast_Annotations() + { + var marker = new DebugStepMarker(); + 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); + + // A non-marker annotation is not bridged: only the debug-step marker is queried by the + // resolver, and indexing arbitrary shared annotations would collide by reference identity. + var otherAnnotation = new object(); + var otherNode = new IdentifierExpression("x"); + otherNode.AddAnnotation(otherAnnotation); + lookup.AddNode(otherNode, 30, 1); + lookup.TryGetRange(otherAnnotation, out _).Should().BeFalse( + "only DebugStepMarker annotations are bridged to a text range"); + return Task.CompletedTask; + } + + [AvaloniaTest] + public Task DebugStepHighlighter_Removal_Resolves_To_Seam_Caret() + { + // A step whose node was removed has no precise range and (in this fixture) no rendered + // ancestor, only surviving neighbors. It must resolve to a zero-length caret at the gap: + // the successor's start when one survives, otherwise the predecessor's end. + var successor = new object(); + var predecessor = new object(); + + var successorLookup = new NodeLookup(); + successorLookup.AddNode(successor, 40, 6); + var removalWithSuccessor = new Stepper.Node("0: Remove statement") { + BeginStep = 0, + EndStep = 1 + }; + removalWithSuccessor.SeamAnchors.Add((successor, false)); + removalWithSuccessor.SeamAnchors.Add((predecessor, true)); + var successorStepper = new Stepper(); + successorStepper.Steps.Add(removalWithSuccessor); + + DebugStepHighlighter.TryResolve(successorStepper, stepLimit: 1, highlightStep: 0, successorLookup, out var caret) + .Should().BeTrue("a removed node must still resolve to a surviving seam neighbor"); + caret.Length.Should().Be(0, "a removal has no text to highlight, only a caret at the gap"); + caret.Start.Should().Be(40, "the caret sits at the successor's start, where the node was"); + + // Only the predecessor survives: the caret sits at its end (10 + 5). + var predecessorLookup = new NodeLookup(); + predecessorLookup.AddNode(predecessor, 10, 5); + var removalWithPredecessor = new Stepper.Node("0: Remove statement") { + BeginStep = 0, + EndStep = 1 + }; + removalWithPredecessor.SeamAnchors.Add((successor, false)); + removalWithPredecessor.SeamAnchors.Add((predecessor, true)); + var predecessorStepper = new Stepper(); + predecessorStepper.Steps.Add(removalWithPredecessor); + + DebugStepHighlighter.TryResolve(predecessorStepper, stepLimit: 1, highlightStep: 0, predecessorLookup, out var endCaret) + .Should().BeTrue("a removed node must fall back to a surviving predecessor"); + endCaret.Length.Should().Be(0); + endCaret.Start.Should().Be(15, "with no successor, the caret sits at the predecessor's end"); + return Task.CompletedTask; + } + + [AvaloniaTest] + public Task DebugStepFilter_Keeps_Matches_And_The_Path_To_Them() + { + var converter = new DebugStepFilterConverter(); + var matchingLeaf = new Stepper.Node("3: Introduce query continuation"); + var otherLeaf = new Stepper.Node("4: Flatten switch section block"); + var group = new Stepper.Node("CombineQueryExpressions"); + group.Children.Add(matchingLeaf); + group.Children.Add(otherLeaf); + + // An empty filter shows every row. + Filter(group, "").Should().BeTrue(); + Filter(otherLeaf, " ").Should().BeTrue(); + // A group survives because a descendant matches, keeping the path to the match. + Filter(group, "continuation").Should().BeTrue(); + // The matching leaf survives, case-insensitively. + Filter(matchingLeaf, "CONTINUATION").Should().BeTrue(); + // A sibling that neither matches nor leads to a match is hidden. + Filter(otherLeaf, "continuation").Should().BeFalse(); + return Task.CompletedTask; + + bool Filter(Stepper.Node node, string filter) + => (bool)converter.Convert(new object?[] { node, filter }, typeof(bool), null, CultureInfo.InvariantCulture); + } + + [AvaloniaTest] + public Task DebugSteps_View_Loads_With_Filter_Applied() + { + // Guards the filter wiring in the XAML -- a MultiBinding inside a TreeViewItem style Setter + // plus the RelativeSource lookups -- against a structural break that x:CompileBindings="False" + // would not catch at build time. Realising the view with a populated tree and a live filter + // must not throw. + var vm = new DebugStepsPaneModel(); + var group = new Stepper.Node("CombineQueryExpressions"); + group.Children.Add(new Stepper.Node("3: Introduce query continuation")); + group.Children.Add(new Stepper.Node("4: Flatten switch section block")); + vm.Steps = new[] { group }; + vm.IsAvailable = true; + + var window = new Window { Width = 400, Height = 300, Content = new DebugSteps { DataContext = vm } }; + window.Show(); + vm.FilterText = "continuation"; + Dispatcher.UIThread.RunJobs(); + window.Close(); + return Task.CompletedTask; + } + + // A replay highlight must land on the changed node, not merely be non-null: in bounds, not a + // flood of the whole document, and (unless it is a zero-length removal caret) on rendered code + // rather than whitespace. This is what keeps the ancestor fallback from silently widening every + // highlight to the enclosing method undetected. + static void AssertPreciseHighlight(DecompilerTabPageModel tab, string because) + { + tab.DebugStepHighlight.Should().NotBeNull(because); + var range = tab.DebugStepHighlight!.Value; + var text = tab.Text!; + (range.Start >= 0 && range.Start + range.Length <= text.Length).Should() + .BeTrue("the debug-step highlight must lie within the emitted document"); + range.Length.Should().BeLessThan(text.Length, + "the highlight must mark a specific node, not flood the whole document"); + if (range.Length > 0) + { + text.Substring(range.Start, range.Length).Trim().Should() + .NotBeEmpty("a non-caret highlight must cover rendered code, not just whitespace"); + } + } + + [AvaloniaTest] + public Task MarkNodeStart_Does_Not_Record_When_Node_Tracking_Is_Disabled() + { + var output = new AvaloniaEditTextOutput(); + var node = new object(); + + output.MarkNodeStart(node); + output.Write("statement;"); + output.MarkNodeEnd(node); + + output.NodeLookup.TryGetRange(node, out _).Should().BeFalse(); + return Task.CompletedTask; + } + + [AvaloniaTest] + public Task MarkNodeStart_Excludes_Leading_Indentation() + { + // A node opened at the start of an indented line must record its range from the first real + // character, so the debug-step highlight does not extend across the indentation to column 0. + var output = new AvaloniaEditTextOutput { EnableNodeTracking = true }; + output.Indent(); + output.WriteLine(); + + var node = new object(); + output.MarkNodeStart(node); + output.Write("statement;"); + output.MarkNodeEnd(node); + + output.NodeLookup.TryGetRange(node, out var range).Should().BeTrue(); + output.GetText().Substring(range.Start, range.Length).Should().Be("statement;"); + return Task.CompletedTask; + } + + [AvaloniaTest] + public Task MarkNodeEnd_Records_Nodes_Regardless_Of_Close_Order() + { + // Node spans are keyed by identity, so closing an outer node before the inner one it still + // contains must not discard either range. A stack that popped by position would lose both. + var output = new AvaloniaEditTextOutput { EnableNodeTracking = true }; + var outer = new object(); + var inner = new object(); + + output.MarkNodeStart(outer); + output.Write("a("); + output.MarkNodeStart(inner); + output.Write("b"); + output.MarkNodeEnd(outer); + output.Write(")"); + output.MarkNodeEnd(inner); + + output.NodeLookup.TryGetRange(outer, out var outerRange).Should().BeTrue(); + output.GetText().Substring(outerRange.Start, outerRange.Length).Should().Be("a(b"); + output.NodeLookup.TryGetRange(inner, out var innerRange).Should().BeTrue(); + output.GetText().Substring(innerRange.Start, innerRange.Length).Should().Be("b)"); + return Task.CompletedTask; + } + [AvaloniaTest] public Task Pane_Reports_Not_Available_For_Languages_Without_Debug_Steps() { @@ -148,7 +483,7 @@ public Task Pane_Reports_Not_Available_For_Languages_Without_Debug_Steps() debugStepsVm.IsAvailable.Should().BeTrue("C# provides debug steps"); // Simulate a populated tree from the C# run, then flip to the disassembler language. - debugStepsVm.Steps = new[] { new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("stale") }; + debugStepsVm.Steps = new[] { new Stepper.Node("stale") }; languageService.CurrentLanguage = languageService.Languages.OfType().First(l => l.Name == "IL"); global::Avalonia.Threading.Dispatcher.UIThread.RunJobs(); diff --git a/ILSpy/DecompilationOptions.cs b/ILSpy/DecompilationOptions.cs index 1c205b61ce..2e076ec245 100644 --- a/ILSpy/DecompilationOptions.cs +++ b/ILSpy/DecompilationOptions.cs @@ -60,6 +60,13 @@ public sealed class DecompilationOptions /// 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 eb84056d18..09684fcfd3 100644 --- a/ILSpy/Languages/CSharpHighlightingTokenWriter.cs +++ b/ILSpy/Languages/CSharpHighlightingTokenWriter.cs @@ -124,6 +124,12 @@ public CSharpHighlightingTokenWriter(TokenWriter decoratedWriter, ISmartTextOutp //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 @@ public override void WritePrimitiveValue(object? value, ICSharpCode.Decompiler.C public override void StartNode(AstNode node) { + nodeTrackingOutput?.MarkNodeStart(node); nodeStack.Push(node); base.StartNode(node); } @@ -503,6 +510,7 @@ public override void StartNode(AstNode node) public override void EndNode(AstNode node) { base.EndNode(node); + nodeTrackingOutput?.MarkNodeEnd(node); nodeStack.Pop(); } @@ -511,6 +519,7 @@ public override void EndNode(AstNode node) 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 a852d6d8a2..6d350cee4f 100644 --- a/ILSpy/Languages/CSharpLanguage.DebugSteps.cs +++ b/ILSpy/Languages/CSharpLanguage.DebugSteps.cs @@ -22,7 +22,7 @@ using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; -using ICSharpCode.Decompiler.IL.Transforms; +using ICSharpCode.Decompiler.DebugSteps; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Docking; @@ -32,18 +32,16 @@ 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 @@ partial class CSharpLanguage : IDebugStepProvider // 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 @@ partial void OnCSharpDecompiled(ITextOutput output, DecompilationOptions options // 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 73a2fcc0a1..d017b515aa 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -32,6 +32,7 @@ using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; +using ICSharpCode.Decompiler.DebugSteps; using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Output; @@ -313,11 +314,8 @@ CSharpDecompiler CreateDecompiler(MetadataFile module, DecompilationOptions opti 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 @@ public override void DecompileMethod(IMethod method, ITextOutput output, Decompi { 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.Stepper); } else { - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(method.MetadataToken)); + WriteCode(output, options, decompiler.Decompile(method.MetadataToken), decompiler.Stepper); } - 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.Stepper); + OnCSharpDecompiled(decompiler, output, options); } public override void DecompileField(IField field, ITextOutput output, DecompilationOptions options) @@ -372,16 +370,16 @@ public override void DecompileField(IField field, ITextOutput output, Decompilat 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.Stepper); } 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.Stepper); } - OnCSharpDecompiled(output, options); + OnCSharpDecompiled(decompiler, output, options); } /// @@ -408,24 +406,24 @@ void DecompileExtensionCore(IEntity extension, IType commentType, ITextOutput ou 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.Stepper); + 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.Stepper); + 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.Stepper); + OnCSharpDecompiled(decompiler, output, options); } public override ProjectId? DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) @@ -511,7 +509,7 @@ public override void DecompileType(ITypeDefinition type, ITextOutput output, Dec SyntaxTree st = options.FullDecompilation ? decompiler.DecompileWholeModuleAsSingleFile() : decompiler.DecompileModuleAndAssemblyAttributes(); - WriteCode(output, options.DecompilerSettings, decompiler, st); + WriteCode(output, options, st, decompiler.Stepper); return null; } @@ -709,13 +707,19 @@ public void Run(AstNode rootNode, TransformContext context) } } - static void WriteCode(ITextOutput output, DecompilerSettings settings, CSharpDecompiler decompiler, SyntaxTree syntaxTree) + static void WriteCode(ITextOutput output, DecompilationOptions options, SyntaxTree syntaxTree, Stepper stepper) { + 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) + TokenWriter tokenWriter = new TextTokenWriter(output, settings); + // Node-range tracking (NodeLookup) is only consumed by the debug-step highlighter, which + // resolves nothing without a step limit. Skip it on a normal decompile so the common path + // doesn't pay the per-node/per-annotation bookkeeping; AvaloniaEditTextOutput is an + // ISmartTextOutput, so the branch below still gives it full syntax highlighting. + if (output is TextView.AvaloniaEditTextOutput avaloniaOutput && options.StepLimit != int.MaxValue) + 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 +732,11 @@ static void WriteCode(ITextOutput output, DecompilerSettings settings, CSharpDec syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); bookmarkCollector?.Publish(); + if (output is TextView.AvaloniaEditTextOutput nodeOutput + && TextView.DebugStepHighlighter.TryResolve(stepper, options.StepLimit, options.HighlightStep, nodeOutput.NodeLookup, out var range)) + { + nodeOutput.DebugStepHighlight = range; + } } void AddWarningMessage(MetadataFile module, ITextOutput output, string line1, string? line2 = null, diff --git a/ILSpy/Languages/IDebugStepProvider.cs b/ILSpy/Languages/IDebugStepProvider.cs index 8316f55620..465b85bc4e 100644 --- a/ILSpy/Languages/IDebugStepProvider.cs +++ b/ILSpy/Languages/IDebugStepProvider.cs @@ -20,7 +20,7 @@ using System; -using ICSharpCode.Decompiler.IL.Transforms; +using ICSharpCode.Decompiler.DebugSteps; namespace ICSharpCode.ILSpy.Languages { diff --git a/ILSpy/Languages/ILAstLanguage.cs b/ILSpy/Languages/ILAstLanguage.cs index ff11e669ce..fe4ff0dde8 100644 --- a/ILSpy/Languages/ILAstLanguage.cs +++ b/ILSpy/Languages/ILAstLanguage.cs @@ -24,6 +24,7 @@ using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.DebugSteps; using ICSharpCode.Decompiler.Disassembler; using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.IL.Transforms; @@ -188,6 +189,11 @@ public override void DecompileMethod(IMethod method, ITextOutput output, Decompi }); output.WriteLine(); il.WriteTo(output, DebugStepsPaneModel.WritingOptions); + if (output is TextView.AvaloniaEditTextOutput nodeOutput + && TextView.DebugStepHighlighter.TryResolve(context.Stepper, options.StepLimit, options.HighlightStep, nodeOutput.NodeLookup, out var range)) + { + nodeOutput.DebugStepHighlight = range; + } } } } diff --git a/ILSpy/TextView/AvaloniaEditTextOutput.cs b/ILSpy/TextView/AvaloniaEditTextOutput.cs index 9c04fcf48c..57a321d11f 100644 --- a/ILSpy/TextView/AvaloniaEditTextOutput.cs +++ b/ILSpy/TextView/AvaloniaEditTextOutput.cs @@ -42,7 +42,7 @@ namespace ICSharpCode.ILSpy.TextView /// Reference markers, fold ranges, and inline UI elements are intentionally dropped here — /// those land when we add hyperlinks and folding support. /// - public sealed class AvaloniaEditTextOutput : ISmartTextOutput + public sealed class AvaloniaEditTextOutput : ISmartTextOutput, INodeTrackingOutput { readonly StringBuilder builder = new(); @@ -54,6 +54,10 @@ public sealed class AvaloniaEditTextOutput : ISmartTextOutput public int LengthLimit { get; set; } = int.MaxValue; readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new(); + // Keyed by node rather than a stack: node writes are strictly nested today, but keying by + // identity means a stray or out-of-order MarkNodeEnd is a clean no-op that can't desync the + // remaining open nodes. Each node is written exactly once, so there is no self-nesting. + readonly Dictionary openNodeStarts = new(ReferenceEqualityComparer.Instance); readonly Stack<(NewFolding Folding, int StartLine)> openFoldings = new(); readonly List foldings = new(); int indent; @@ -75,6 +79,12 @@ public sealed class AvaloniaEditTextOutput : ISmartTextOutput /// The highlighting spans referencing the live named colours; see the field note. public IReadOnlyList<(int Start, int Length, HighlightingColor Color)> HighlightingSpans => highlightingSpans; + /// + /// Enables syntax-node range collection for debug-step highlighting. Normal decompiles leave this + /// off because node ranges are only consumed by step-limited replay output. + /// + public bool EnableNodeTracking { get; set; } + /// Foldings collected during writing — only ones spanning more than one line. public IReadOnlyList Foldings => foldings; @@ -87,6 +97,10 @@ public sealed class AvaloniaEditTextOutput : ISmartTextOutput /// 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(); /// @@ -285,6 +299,7 @@ public void AddVisualLineElementGenerator(VisualLineElementGenerator generator) public void BeginSpan(HighlightingColor highlightingColor) { + WriteIndentIfNeeded(); openSpans.Push((builder.Length, highlightingColor)); } @@ -300,5 +315,24 @@ public void EndSpan() highlightingSpans.Add((start, length, color)); } } + + public void MarkNodeStart(object node) + { + if (!EnableNodeTracking) + return; + // Flush a pending indent before capturing the offset so a node opened at the start of a + // line records its range from the first real character, not from column 0 across the + // leading indentation. + WriteIndentIfNeeded(); + openNodeStarts[node] = builder.Length; + } + + public void MarkNodeEnd(object node) + { + if (!EnableNodeTracking) + return; + if (openNodeStarts.Remove(node, out var start)) + NodeLookup.AddNode(node, start, builder.Length - start); + } } } diff --git a/ILSpy/TextView/DebugStepHighlighter.cs b/ILSpy/TextView/DebugStepHighlighter.cs new file mode 100644 index 0000000000..650a9be4fb --- /dev/null +++ b/ILSpy/TextView/DebugStepHighlighter.cs @@ -0,0 +1,93 @@ +// 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 ICSharpCode.Decompiler.DebugSteps; + +namespace ICSharpCode.ILSpy.TextView +{ + /// + /// Maps a debug-stepper step to the character range of the node it changed, using the + /// built while rendering. Language-agnostic: it resolves a step's + /// (and ) + /// against the lookup, so it serves both the C# AST and ILAst views. + /// + internal static class DebugStepHighlighter + { + /// + /// Resolves the range to highlight for a step-limited decompile. + /// and come from the re-decompile request; the step is + /// looked up in (the instance the transforms populated). + /// Returns false for a full run (no step limit) or when no candidate has a recorded range. + /// + public static bool TryResolve(Stepper stepper, int stepLimit, int? highlightStep, NodeLookup nodeLookup, out TextRange range) + { + range = default; + if (stepLimit == int.MaxValue) + return false; + if (highlightStep is { } step) + { + if (TryGetRange(stepper.GetStepByBeginStep(step), nodeLookup, out range)) + return true; + if (stepper.LimitReachedStep is { BeginStep: var limitStep } reachedStep + && limitStep == step) + { + return TryGetRange(reachedStep, nodeLookup, out range); + } + return false; + } + if (TryGetRange(stepper.LimitReachedStep, nodeLookup, out range)) + return true; + if (stepLimit > 0) + return TryGetRange(stepper.GetStepByBeginStep(stepLimit - 1), nodeLookup, out range); + return false; + } + + static bool TryGetRange(Stepper.Node? step, NodeLookup nodeLookup, out TextRange range) + { + range = default; + if (step == null) + return false; + // Precise identities of the changed node (node, marker, produced node). + foreach (var candidate in step.ModifiedNodeCandidates) + { + if (nodeLookup.TryGetRange(candidate, out range)) + return true; + } + if (step.ModifiedNode != null && nodeLookup.TryGetRange(step.ModifiedNode, out range)) + return true; + // The node itself is gone (a removal): point a zero-length caret at the gap it left, + // anchored to a surviving neighbor, rather than flooding the enclosing block. + foreach (var (anchor, atEnd) in step.SeamAnchors) + { + if (nodeLookup.TryGetRange(anchor, out var anchorRange)) + { + int caret = atEnd ? anchorRange.Start + anchorRange.Length : anchorRange.Start; + range = new TextRange(caret, 0); + return true; + } + } + // Last resort: the enclosing block, so the "show state before" view still lands somewhere. + foreach (var candidate in step.AncestorCandidates) + { + if (nodeLookup.TryGetRange(candidate, out range)) + return true; + } + return false; + } + } +} diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index 397dff4b42..74be6b190b 100644 --- a/ILSpy/TextView/DecompilerTabPageModel.cs +++ b/ILSpy/TextView/DecompilerTabPageModel.cs @@ -183,6 +183,9 @@ void ResetProgress() [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 @@ public DecompilerTabPageModel() // 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,11 +401,12 @@ public DecompilerTabPageModel() /// is ). toggles the transforms' /// verbose-debug emission. No-op when there's nothing currently being decompiled. /// - public void RestartDecompileWithStepLimit(int stepLimit, bool isDebug) + public Task RestartDecompileWithStepLimit(int stepLimit, bool isDebug, int? highlightStep = null) { pendingStepLimit = stepLimit; + pendingHighlightStep = highlightStep; pendingIsDebug = isDebug; - StartDecompile(); + return StartDecompile(); } /// Re-runs the current decompile with a larger output-length limit (the "Display code @@ -458,16 +463,20 @@ void SaveCurrentNode() ICSharpCode.ILSpy.Commands.SaveCodeHelper.SaveNodeAsync(node, languageService, dockWorkspace).HandleExceptions(); } - // Fire-and-forget wrapper around DecompileAsync that observes the resulting Task. - // Without this, exceptions raised by the dispatched property setters (e.g. the - // PropertyChanged subscribers in DecompilerTextView) become UnobservedTaskException - // faults that the finalizer thread rethrows much later, hiding the originating bug. - void StartDecompile() + // Starts a decompile and returns the Task that completes once the output has been applied, + // so callers (e.g. the Debug Steps replay, and tests) can await completion deterministically + // instead of polling IsDecompiling/Text. A fault-observing continuation is attached so that + // exceptions raised by the dispatched property setters (e.g. the PropertyChanged subscribers + // in DecompilerTextView) don't become UnobservedTaskException faults the finalizer thread + // rethrows much later, hiding the originating bug. + Task StartDecompile() { - pendingDecompile = DecompileAsync().ContinueWith(t => { + var decompile = DecompileAsync(); + pendingDecompile = decompile.ContinueWith(t => { if (t.Exception is { } ex) System.Diagnostics.Debug.WriteLine($"DecompileAsync faulted: {ex.Flatten()}"); }, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); + return decompile; } /// @@ -511,6 +520,7 @@ async Task DecompileAsync() References = null; DefinitionLookup = null; DebugInfo = null; + DebugStepHighlight = null; UIElements = null; Text = string.Empty; IsDecompiling = false; @@ -537,8 +547,10 @@ async Task DecompileAsync() // 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; @@ -546,13 +558,17 @@ async Task DecompileAsync() using (ICSharpCode.ILSpy.AppEnv.AppLog.Phase($"DecompileAsync #{callNumber}: Task.Run decompile body ({nodes.Count} node(s), language={language.Name})")) { (output, _) = await Task.Run(() => { - var output = new AvaloniaEditTextOutput { LengthLimit = outputLengthLimit }; + var output = new AvaloniaEditTextOutput { + LengthLimit = outputLengthLimit, + EnableNodeTracking = stepLimit != int.MaxValue, + }; // decompilerSettings is null only in design-time / minimal test hosts // without composition; fall back to defaults there. var options = new DecompilationOptions( decompilerSettings ?? new ICSharpCode.Decompiler.DecompilerSettings()) { CancellationToken = cts.Token, StepLimit = stepLimit, + HighlightStep = highlightStep, IsDebug = isDebug, }; try @@ -745,6 +761,7 @@ void ApplyOutput(AvaloniaEditTextOutput output, string syntaxExtension, string t : 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 54eb45e70f..4e6fc469b3 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -64,6 +64,7 @@ public partial class DecompilerTextView : UserControl // 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,8 @@ public partial class DecompilerTextView : UserControl TextMarkerService textMarkerService = null!; BracketHighlightRenderer bracketHighlightRenderer = null!; readonly List localReferenceMarks = new(); + readonly List debugStepMarks = new(); + int debugStepHighlightVersion; readonly List activeCustomGenerators = new(); RichTextColorizer? activeColorizer; FoldingManager? activeFoldingManager; @@ -1043,6 +1046,7 @@ void ApplyDocument(DecompilerTabPageModel model, bool restoreViewState = true) // 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 +1076,8 @@ void ApplyDocument(DecompilerTabPageModel model, bool restoreViewState = true) 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 +1252,49 @@ void ClearLocalReferenceMarks() 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); + // A zero-length range is a seam caret: a step that removed its node leaves nothing to + // colour in the resulting text, so only the caret is placed and pulsed at the gap. + if (r.Length > 0) + { + var end = Math.Clamp(r.Start + r.Length, start, Editor.Document.TextLength); + if (end > start) + { + var mark = textMarkerService.Create(start, end - start); + mark.BackgroundColor = DebugStepBackground; + debugStepMarks.Add(mark); + } + } + Editor.TextArea.Caret.Offset = start; + Editor.TextArea.Caret.BringCaretToView(); + // Centre on the changed node once layout has caught up, via the same helper bookmark + // navigation uses: GetVisualTopByDocumentLine accounts for collapsed foldings (dense in + // ILAst block output) that a logical line*height calc would misplace, Background priority + // lets a just-applied document finish measuring, and CenterLineInView rechecks the document + // so a newer decompile landing before the post runs can't scroll the wrong content. + var document = Editor.Document; + var line = document.GetLineByOffset(start).LineNumber; + var highlightVersion = debugStepHighlightVersion; + Dispatcher.UIThread.Post(() => { + if (highlightVersion == debugStepHighlightVersion) + CenterLineInView(document, line); + }, DispatcherPriority.Background); + CaretHighlightAdorner.DisplayCaretHighlightAnimation(Editor.TextArea); + } + + void ClearDebugStepMarks() + { + debugStepHighlightVersion++; + 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 0000000000..a03f39b3b3 --- /dev/null +++ b/ILSpy/TextView/NodeLookup.cs @@ -0,0 +1,56 @@ +// 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; +using ICSharpCode.Decompiler.DebugSteps; + +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) + return; + var range = new TextRange(start, length); + nodes[node] = range; + // Bridge only the debug-step marker: it is the sole annotation the resolver ever looks up + // (it rides CopyAnnotationsFrom so a replaced node's step still resolves to the emitted + // text). Indexing every annotation would add dead keys never queried, and would let a + // shared annotation resolve to whichever node rendered last. + if (node is IAnnotatable annotatable) + { + foreach (var annotation in annotatable.Annotations) + { + if (annotation is DebugStepMarker) + nodes[annotation] = range; + } + } + } + } +} diff --git a/ILSpy/TextView/TextRange.cs b/ILSpy/TextView/TextRange.cs new file mode 100644 index 0000000000..2eadc7426c --- /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 f58c107674..0138418525 100644 --- a/ILSpy/ViewModels/DebugStepsPaneModel.cs +++ b/ILSpy/ViewModels/DebugStepsPaneModel.cs @@ -27,8 +27,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using ICSharpCode.Decompiler.DebugSteps; using ICSharpCode.Decompiler.IL; -using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Commands; @@ -100,6 +100,20 @@ public sealed partial class DebugStepsPaneModel : ToolPaneModel [ObservableProperty] bool isAvailable; + /// + /// Free-text filter for the step tree; empty shows everything. Bound to the filter box in the + /// pane's top-right corner. A row survives when its description -- or a descendant's -- matches. + /// + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsFiltering))] + string? filterText; + + /// + /// True while is non-empty. Drives auto-expansion of the tree so that + /// matches nested under transform groups are revealed rather than hidden in collapsed groups. + /// + public bool IsFiltering => !string.IsNullOrWhiteSpace(FilterText); + public IRelayCommand ShowStateBeforeCommand { get; } public IRelayCommand ShowStateAfterCommand { get; } public IRelayCommand DebugStepCommand { get; } @@ -109,8 +123,8 @@ public DebugStepsPaneModel() { 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 +137,7 @@ public DebugStepsPaneModel() 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); }); } @@ -206,12 +220,26 @@ void OnStepperUpdated(object? sender, System.EventArgs e) void OnSelectionChanged(object? sender, AssemblyTreeSelectionChangedEventArgs e) { - // User picked a new tree node — the previous run's stepper is stale until the - // next decompile populates it. - Dispatcher.UIThread.Post(() => { + // User picked a new tree node — the previous run's stepper is stale until the next + // decompile populates it. Clear synchronously (this message is raised on the UI thread, + // right as the new selection's decompile is kicked off) so the blanking is pinned before + // that decompile can finish and post its StepperUpdated. A deferred clear could otherwise + // float to a dispatcher cycle after the populate and wipe the fresh steps, leaving the + // pane empty until the next decompile — the intermittent "no steps" race. + if (Dispatcher.UIThread.CheckAccess()) + { + ClearSteps(); + } + else + { + Dispatcher.UIThread.Post(ClearSteps); + } + + void ClearSteps() + { Steps = null; lastSelectedStep = int.MaxValue; - }); + } } void OnWritingOptionsChanged(object? sender, PropertyChangedEventArgs e) @@ -222,12 +250,12 @@ void OnWritingOptionsChanged(object? sender, PropertyChangedEventArgs e) 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); } } } diff --git a/ILSpy/Views/DebugStepFilterConverter.cs b/ILSpy/Views/DebugStepFilterConverter.cs new file mode 100644 index 0000000000..5e78a9ccfe --- /dev/null +++ b/ILSpy/Views/DebugStepFilterConverter.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#if DEBUG + +using System; +using System.Collections.Generic; +using System.Globalization; + +using Avalonia.Data.Converters; + +using ICSharpCode.Decompiler.DebugSteps; + +namespace ICSharpCode.ILSpy.Views +{ + /// + /// Decides whether a Debug Steps tree row stays visible under the pane's filter box. A step is + /// shown when the filter is empty, or when its description -- or that of any descendant -- + /// contains the filter text, so the path to every match is preserved. Bound per row against + /// [ the step, the filter text ]. + /// + public sealed class DebugStepFilterConverter : IMultiValueConverter + { + public object Convert(IList values, Type targetType, object? parameter, CultureInfo culture) + { + if (values.Count < 2 || values[1] is not string filter || string.IsNullOrWhiteSpace(filter)) + return true; + return values[0] is Stepper.Node node && Matches(node, filter.Trim()); + } + + static bool Matches(Stepper.Node node, string filter) + { + if (node.Description.Contains(filter, StringComparison.OrdinalIgnoreCase)) + return true; + foreach (var child in node.Children) + { + if (Matches(child, filter)) + return true; + } + return false; + } + } +} + +#endif diff --git a/ILSpy/Views/DebugSteps.axaml b/ILSpy/Views/DebugSteps.axaml index 44144bf946..5919970546 100644 --- a/ILSpy/Views/DebugSteps.axaml +++ b/ILSpy/Views/DebugSteps.axaml @@ -3,40 +3,63 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:ICSharpCode.ILSpy.ViewModels" + xmlns:views="using:ICSharpCode.ILSpy.Views" xmlns:il="using:ICSharpCode.Decompiler.IL" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="300" x:Class="ICSharpCode.ILSpy.Views.DebugSteps" x:DataType="vm:DebugStepsPaneModel"> + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/ILSpy/Views/DebugSteps.axaml.cs b/ILSpy/Views/DebugSteps.axaml.cs index 227b6a5b70..cf6de5de0e 100644 --- a/ILSpy/Views/DebugSteps.axaml.cs +++ b/ILSpy/Views/DebugSteps.axaml.cs @@ -37,6 +37,11 @@ public partial class DebugSteps : UserControl public DebugSteps() { InitializeComponent(); + // TreeViewItem.OnKeyDown consumes Enter/Return to expand or collapse the focused row + // (marking the event handled) before it bubbles, so a bubble-phase handler never sees + // Enter on a group row. Intercept in the tunnel phase, ahead of the item, so Enter and + // Shift+Enter drive the show-state commands for both leaf and group steps. + StepsTree.AddHandler(InputElement.KeyDownEvent, OnTreeKeyDown, RoutingStrategies.Tunnel, handledEventsToo: true); } void OnTreeDoubleTapped(object? sender, TappedEventArgs e)