From d22d2cffa06ebfc02e1e56de7dae15fb8a55311f Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 27 Jun 2026 07:49:13 +0200 Subject: [PATCH 01/21] Add fine-grained C# AST debug steps Record AST transform groups and mutation steps through the C# pipeline, replay selected steps with the stepper, and carry modified-node ranges through output so the Debug Steps pane can highlight the selected mutation without replacing its full step tree. Assisted-by: CodeAlta:gpt-5.5:CodeAlta --- .../TestCases/Pretty/OutVariables.cs | 11 +++ .../CSharp/CSharpDecompiler.cs | 26 +++-- .../CSharp/Transforms/AddCheckedBlocks.cs | 33 ++++--- .../AddXmlDocumentationTransform.cs | 1 + .../Transforms/CombineQueryExpressions.cs | 20 +++- .../CSharp/Transforms/DeclareVariables.cs | 97 +++++++++++-------- .../Transforms/EscapeInvalidIdentifiers.cs | 7 +- .../CSharp/Transforms/FixNameCollisions.cs | 2 + .../CSharp/Transforms/FlattenSwitchBlocks.cs | 1 + .../Transforms/IntroduceExtensionMethods.cs | 9 ++ .../Transforms/IntroduceQueryExpressions.cs | 16 ++- .../Transforms/IntroduceUnsafeModifier.cs | 22 ++++- .../Transforms/IntroduceUsingDeclarations.cs | 32 +++++- .../Transforms/NormalizeBlockStatements.cs | 12 ++- .../Transforms/PatternStatementTransform.cs | 24 +++++ .../CSharp/Transforms/PrettifyAssignments.cs | 11 ++- .../ReplaceMethodCallsWithOperators.cs | 62 +++++++----- .../CSharp/Transforms/TransformContext.cs | 71 ++++++++++++++ ...ransformFieldAndConstructorInitializers.cs | 30 +++++- .../IL/Transforms/Stepper.cs | 47 +++++++-- ILSpy.Tests/Views/DebugStepsTests.cs | 93 ++++++++++++++++++ ILSpy/DecompilationOptions.cs | 7 ++ .../CSharpHighlightingTokenWriter.cs | 9 ++ ILSpy/Languages/CSharpLanguage.DebugSteps.cs | 29 ++---- ILSpy/Languages/CSharpLanguage.cs | 89 ++++++++++++----- ILSpy/TextView/AvaloniaEditTextOutput.cs | 20 ++++ ILSpy/TextView/DecompilerTabPageModel.cs | 12 ++- ILSpy/TextView/DecompilerTextView.axaml.cs | 30 ++++++ ILSpy/TextView/NodeLookup.cs | 50 ++++++++++ ILSpy/TextView/TextRange.cs | 22 +++++ ILSpy/ViewModels/DebugStepsPaneModel.cs | 10 +- 31 files changed, 746 insertions(+), 159 deletions(-) create mode 100644 ILSpy/TextView/NodeLookup.cs create mode 100644 ILSpy/TextView/TextRange.cs 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..7ef08ee924 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -176,6 +176,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 +716,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..ba0040d138 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUnsafeModifier.cs @@ -29,9 +29,19 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { public class IntroduceUnsafeModifier : DepthFirstAstVisitor, IAstTransform { + TransformContext? context; + public void Run(AstNode compilationUnit, TransformContext context) { - compilationUnit.AcceptVisitor(this); + this.context = context; + try + { + compilationUnit.AcceptVisitor(this); + } + finally + { + this.context = null; + } } public static bool IsUnsafe(AstNode node) @@ -52,6 +62,8 @@ protected override bool VisitChildren(AstNode node) } if (result && node is EntityDeclaration && !(node is Accessor)) { + if (context != null) + context.Step("Add unsafe modifier", node); ((EntityDeclaration)node).Modifiers |= Modifiers.Unsafe; return false; } @@ -95,6 +107,8 @@ public override bool VisitUnaryOperatorExpression(UnaryOperatorExpression unaryO && bop.GetResolveResult() is OperatorResolveResult orr && orr.Operands.FirstOrDefault()?.Type.Kind == TypeKind.Pointer) { + if (context != null) + context.Step("Replace pointer addition with indexer", unaryOperatorExpression); // transform "*(ptr + int)" to "ptr[int]" IndexerExpression indexer = new IndexerExpression(); indexer.Target = bop.Left!.Detach(); @@ -102,6 +116,8 @@ public override bool VisitUnaryOperatorExpression(UnaryOperatorExpression unaryO indexer.CopyAnnotationsFrom(unaryOperatorExpression); indexer.CopyAnnotationsFrom(bop); unaryOperatorExpression.ReplaceWith(indexer); + if (context != null) + context.EndStep(indexer); } return true; } @@ -121,6 +137,8 @@ public override bool VisitMemberReferenceExpression(MemberReferenceExpression me UnaryOperatorExpression? uoe = memberReferenceExpression.Target as UnaryOperatorExpression; if (uoe != null && uoe.Operator == UnaryOperatorType.Dereference) { + if (context != null) + context.Step("Replace pointer member access", memberReferenceExpression); PointerReferenceExpression pre = new PointerReferenceExpression(); pre.Target = uoe.Expression.Detach(); pre.MemberName = memberReferenceExpression.MemberName; @@ -129,6 +147,8 @@ public override bool VisitMemberReferenceExpression(MemberReferenceExpression me pre.RemoveAnnotations(); // only copy the ResolveResult from the MRE pre.CopyAnnotationsFrom(memberReferenceExpression); memberReferenceExpression.ReplaceWith(pre); + if (context != null) + context.EndStep(pre); } if (HasUnsafeResolveResult(memberReferenceExpression)) return true; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceUsingDeclarations.cs index 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..bb8d8cb965 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs @@ -19,9 +19,11 @@ #nullable enable using System.Collections.Immutable; +using System.Diagnostics; using System.Threading; using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.Decompiler.CSharp.Transforms @@ -36,6 +38,7 @@ 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 +70,74 @@ 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) + { + TrackModifiedNode(Stepper.Step(description, modifiedNode: near), near); + } + + [Conditional("STEP")] + [DebuggerStepThrough] + internal void StepStartGroup(string description, AstNode? near = null) + { + TrackModifiedNode(Stepper.StartGroup(description, modifiedNode: near), near); + } + + [Conditional("STEP")] + internal void StepEndGroup(bool keepIfEmpty = false) + { + Stepper.EndGroup(keepIfEmpty); + } + + /// + /// Points the most recently recorded step at the node its mutation produced. + /// Call this after a whose modified node only comes into existence + /// during the mutation (e.g. the result of a ReplaceWith or a freshly inserted node). + /// + [Conditional("STEP")] + internal void EndStep(AstNode? modifiedNode) + { + if (Stepper.LastStep != null) + { + Stepper.LastStep.ModifiedNode = modifiedNode; + TrackModifiedNode(Stepper.LastStep, modifiedNode, insertFirst: true); + } + } + + static void TrackModifiedNode(Stepper.Node step, AstNode? modifiedNode, bool insertFirst = false) + { + if (modifiedNode == null) + return; + AddCandidate(step, modifiedNode, insertFirst); + var marker = new DebugStepMarker(); + modifiedNode.AddAnnotation(marker); + AddCandidate(step, marker, insertFirst: false); + for (var parent = modifiedNode.Parent; parent != null; parent = parent.Parent) + { + AddCandidate(step, parent, insertFirst: false); + } + } + + static void AddCandidate(Stepper.Node step, object candidate, bool insertFirst) + { + if (step.ModifiedNodeCandidates.Contains(candidate)) + return; + if (insertFirst) + step.ModifiedNodeCandidates.Insert(0, candidate); + else + step.ModifiedNodeCandidates.Add(candidate); + } + + sealed class DebugStepMarker + { } } } diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformFieldAndConstructorInitializers.cs index 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/IL/Transforms/Stepper.cs b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs index abbdddbf1d..7976749bce 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs @@ -54,6 +54,8 @@ public static bool SteppingAvailable { } public IList Steps => steps; + public Node? LimitReachedStep { get; private set; } + public Node? LastStep { get; private set; } public int StepLimit { get; set; } = int.MaxValue; public bool IsDebug { get; set; } @@ -62,6 +64,8 @@ public class Node { public string Description { get; } public ILInstruction? Position { get; set; } + public object? ModifiedNode { get; set; } + public IList ModifiedNodeCandidates { get; } = new List(); /// /// BeginStep is inclusive. /// @@ -96,39 +100,62 @@ public Stepper() /// May throw in debug mode. /// [DebuggerStepThrough] - public void Step(string description, ILInstruction? near = null) + public Node Step(string description, ILInstruction? near = null, object? modifiedNode = null) { - StepInternal(description, near); + return StepInternal(description, near, modifiedNode ?? near); } [DebuggerStepThrough] - private Node StepInternal(string description, ILInstruction? near) + private Node StepInternal(string description, ILInstruction? near, object? modifiedNode) { + var stepNode = new Node($"{step}: {description}") { + Position = near, + ModifiedNode = modifiedNode, + BeginStep = step, + EndStep = step + 1 + }; if (step == StepLimit) { + LimitReachedStep = stepNode; if (IsDebug) Debugger.Break(); else throw new StepLimitReachedException(); } - var stepNode = new Node($"{step}: {description}") { - Position = near, - BeginStep = step, - EndStep = step + 1 - }; var p = groups.PeekOrDefault(); if (p != null) p.Children.Add(stepNode); else steps.Add(stepNode); + LastStep = stepNode; step++; return stepNode; } [DebuggerStepThrough] - public void StartGroup(string description, ILInstruction? near = null) + public Node StartGroup(string description, ILInstruction? near = null, object? modifiedNode = null) + { + var stepNode = StepInternal(description, near, modifiedNode ?? near); + groups.Push(stepNode); + return stepNode; + } + + public Node? GetStepByBeginStep(int beginStep) { - groups.Push(StepInternal(description, near)); + return FindStep(steps, beginStep); + + static Node? FindStep(IEnumerable nodes, int beginStep) + { + foreach (var node in nodes) + { + if (node.BeginStep == beginStep) + return node; + var child = FindStep(node.Children, beginStep); + if (child != null) + return child; + } + return null; + } } public void EndGroup(bool keepIfEmpty = false) diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 6f719e10af..f836062df1 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -18,6 +18,7 @@ #if DEBUG +using System; using System.Linq; using System.Threading.Tasks; @@ -27,10 +28,14 @@ using AwesomeAssertions; +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.Syntax; + using ICSharpCode.ILSpy; using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Docking; using ICSharpCode.ILSpy.Languages; +using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.Views; @@ -119,6 +124,75 @@ 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!; + + tab.RestartDecompileWithStepLimit(replayStep.BeginStep, isDebug: false, replayStep.BeginStep); + tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + tab.Text.Should().NotBeNullOrWhiteSpace("C# replay before a selected AST mutation step must still emit code"); + tab.DebugStepHighlight.Should().NotBeNull("C# replay before a selected AST mutation step must locate the changed node"); + debugStepsVm.Steps.Should().BeSameAs(collectedSteps, + "a step-limited C# replay must not replace the full step tree shown by the pane"); + + tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep); + tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + tab.Text.Should().NotBeNullOrWhiteSpace("C# replay after a selected AST mutation step must still emit code"); + tab.DebugStepHighlight.Should().NotBeNull("C# replay after a selected AST mutation step must locate the changed node"); + debugStepsVm.Steps.Should().BeSameAs(collectedSteps, + "a step-limited C# replay must preserve the current full-run step tree and selection context"); + + static string StripStepNumber(string description) + { + var separatorIndex = description.IndexOf(": ", StringComparison.Ordinal); + return separatorIndex >= 0 ? description[(separatorIndex + 2)..] : description; + } + } + [AvaloniaTest] public Task ILAst_And_TypedIL_Languages_Are_Registered_In_Debug_Builds() { @@ -133,6 +207,25 @@ public 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 object(); + var original = new IdentifierExpression("old"); + original.AddAnnotation(marker); + var replacement = new IdentifierExpression("new").CopyAnnotationsFrom(original); + var lookup = new NodeLookup(); + + lookup.AddNode(replacement, 12, 3); + + lookup.TryGetRange(marker, out var range).Should().BeTrue( + "C# debug-step markers copied by AST replacements must still resolve to emitted text"); + range.Start.Should().Be(12); + range.Length.Should().Be(3); + return Task.CompletedTask; + } + [AvaloniaTest] public Task Pane_Reports_Not_Available_For_Languages_Without_Debug_Steps() { diff --git a/ILSpy/DecompilationOptions.cs b/ILSpy/DecompilationOptions.cs index 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..13b5c6a272 100644 --- a/ILSpy/Languages/CSharpLanguage.DebugSteps.cs +++ b/ILSpy/Languages/CSharpLanguage.DebugSteps.cs @@ -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..cf6ca92f56 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -33,6 +33,7 @@ using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.IL; +using ICSharpCode.Decompiler.IL.Transforms; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Output; using ICSharpCode.Decompiler.Solution; @@ -313,11 +314,8 @@ 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); } else { - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(method.MetadataToken)); + WriteCode(output, options, decompiler.Decompile(method.MetadataToken), decompiler); } - OnCSharpDecompiled(output, options); + OnCSharpDecompiled(decompiler, output, options); } // Implemented only under DEBUG (CSharpLanguage.DebugSteps.cs) to feed the Debug Steps pane; // a no-op partial in Release. - partial void OnCSharpDecompiled(ITextOutput output, DecompilationOptions options); + partial void OnCSharpDecompiled(CSharpDecompiler decompiler, ITextOutput output, DecompilationOptions options); public override void DecompileProperty(IProperty property, ITextOutput output, DecompilationOptions options) { CSharpDecompiler decompiler = BeginDecompile(property, output, options); WriteCommentLine(output, TypeToString(property.DeclaringType)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(property.MetadataToken)); - OnCSharpDecompiled(output, options); + WriteCode(output, options, decompiler.Decompile(property.MetadataToken), decompiler); + OnCSharpDecompiled(decompiler, output, options); } public override void DecompileField(IField field, ITextOutput output, DecompilationOptions options) @@ -372,16 +370,16 @@ 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); } else { var members = CollectFieldsAndCtors(field.DeclaringTypeDefinition!, field.IsStatic); var resolvedField = decompiler.TypeSystem.MainModule.GetDefinition((FieldDefinitionHandle)field.MetadataToken); decompiler.AstTransforms.Add(new SelectFieldTransform(resolvedField)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(members)); + WriteCode(output, options, decompiler.Decompile(members), decompiler); } - OnCSharpDecompiled(output, options); + OnCSharpDecompiled(decompiler, output, options); } /// @@ -408,24 +406,24 @@ 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); + OnCSharpDecompiled(decompiler, output, options); } public override void DecompileEvent(IEvent ev, ITextOutput output, DecompilationOptions options) { CSharpDecompiler decompiler = BeginDecompile(ev, output, options); WriteCommentLine(output, TypeToString(ev.DeclaringType)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(ev.MetadataToken)); - OnCSharpDecompiled(output, options); + WriteCode(output, options, decompiler.Decompile(ev.MetadataToken), decompiler); + OnCSharpDecompiled(decompiler, output, options); } public override void DecompileType(ITypeDefinition type, ITextOutput output, DecompilationOptions options) { CSharpDecompiler decompiler = BeginDecompile(type, output, options); WriteCommentLine(output, TypeToString(type, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames)); - WriteCode(output, options.DecompilerSettings, decompiler, decompiler.Decompile(type.MetadataToken)); - OnCSharpDecompiled(output, options); + WriteCode(output, options, decompiler.Decompile(type.MetadataToken), decompiler); + OnCSharpDecompiled(decompiler, output, options); } public override ProjectId? DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) @@ -511,7 +509,7 @@ 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); return null; } @@ -709,13 +707,15 @@ 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, CSharpDecompiler decompiler) { + var settings = options.DecompilerSettings; syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); output.IndentationString = settings.CSharpFormattingOptions.IndentationString; - TokenWriter tokenWriter = new TextTokenWriter(output, settings, decompiler.TypeSystem); - if (output is TextView.ISmartTextOutput smartOutput) + if (output is TextView.AvaloniaEditTextOutput avaloniaOutput) + tokenWriter = new CSharpHighlightingTokenWriter(tokenWriter, avaloniaOutput); + else if (output is TextView.ISmartTextOutput smartOutput) tokenWriter = new CSharpHighlightingTokenWriter(tokenWriter, smartOutput); // For the on-screen C# view, harvest the IL-offset/line map for body bookmarks during this @@ -728,6 +728,47 @@ static void WriteCode(ITextOutput output, DecompilerSettings settings, CSharpDec syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); bookmarkCollector?.Publish(); + if (output is TextView.AvaloniaEditTextOutput nodeOutput + && TryGetDebugStepHighlightRange(decompiler, options, nodeOutput.NodeLookup, out var range)) + { + nodeOutput.DebugStepHighlight = range; + } + } + + static bool TryGetDebugStepHighlightRange(CSharpDecompiler decompiler, DecompilationOptions options, TextView.NodeLookup nodeLookup, out TextView.TextRange range) + { + range = default; + if (options.StepLimit == int.MaxValue) + return false; + if (options.HighlightStep is { } highlightStep) + { + if (TryGetRange(decompiler.Stepper.GetStepByBeginStep(highlightStep), nodeLookup, out range)) + return true; + if (decompiler.Stepper.LimitReachedStep is { BeginStep: var limitStep } reachedStep + && limitStep == highlightStep) + { + return TryGetRange(reachedStep, nodeLookup, out range); + } + return false; + } + if (TryGetRange(decompiler.Stepper.LimitReachedStep, nodeLookup, out range)) + return true; + if (options.StepLimit > 0) + return TryGetRange(decompiler.Stepper.GetStepByBeginStep(options.StepLimit - 1), nodeLookup, out range); + return false; + + static bool TryGetRange(Stepper.Node? step, TextView.NodeLookup nodeLookup, out TextView.TextRange range) + { + range = default; + if (step == null) + return false; + foreach (var candidate in step.ModifiedNodeCandidates) + { + if (nodeLookup.TryGetRange(candidate, out range)) + return true; + } + return step.ModifiedNode != null && nodeLookup.TryGetRange(step.ModifiedNode, out range); + } } void AddWarningMessage(MetadataFile module, ITextOutput output, string line1, string? line2 = null, diff --git a/ILSpy/TextView/AvaloniaEditTextOutput.cs b/ILSpy/TextView/AvaloniaEditTextOutput.cs index 9c04fcf48c..6fa7f17478 100644 --- a/ILSpy/TextView/AvaloniaEditTextOutput.cs +++ b/ILSpy/TextView/AvaloniaEditTextOutput.cs @@ -54,6 +54,7 @@ public sealed class AvaloniaEditTextOutput : ISmartTextOutput public int LengthLimit { get; set; } = int.MaxValue; readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new(); + readonly Stack<(object Node, int Offset)> openNodes = new(); readonly Stack<(NewFolding Folding, int StartLine)> openFoldings = new(); readonly List foldings = new(); int indent; @@ -87,6 +88,10 @@ 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(); /// @@ -300,5 +305,20 @@ public void EndSpan() highlightingSpans.Add((start, length, color)); } } + + internal void MarkNodeStart(object node) + { + openNodes.Push((node, builder.Length)); + } + + internal void MarkNodeEnd(object node) + { + if (openNodes.Count == 0) + return; + var (currentNode, start) = openNodes.Pop(); + if (!ReferenceEquals(currentNode, node)) + return; + NodeLookup.AddNode(node, start, builder.Length - start); + } } } diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index 397dff4b42..4091e30128 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,9 +401,10 @@ 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 void RestartDecompileWithStepLimit(int stepLimit, bool isDebug, int? highlightStep = null) { pendingStepLimit = stepLimit; + pendingHighlightStep = highlightStep; pendingIsDebug = isDebug; StartDecompile(); } @@ -511,6 +516,7 @@ async Task DecompileAsync() References = null; DefinitionLookup = null; DebugInfo = null; + DebugStepHighlight = null; UIElements = null; Text = string.Empty; IsDecompiling = false; @@ -537,8 +543,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; @@ -553,6 +561,7 @@ async Task DecompileAsync() decompilerSettings ?? new ICSharpCode.Decompiler.DecompilerSettings()) { CancellationToken = cts.Token, StepLimit = stepLimit, + HighlightStep = highlightStep, IsDebug = isDebug, }; try @@ -745,6 +754,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..48b0e3d137 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,7 @@ public partial class DecompilerTextView : UserControl TextMarkerService textMarkerService = null!; BracketHighlightRenderer bracketHighlightRenderer = null!; readonly List localReferenceMarks = new(); + readonly List debugStepMarks = new(); readonly List activeCustomGenerators = new(); RichTextColorizer? activeColorizer; FoldingManager? activeFoldingManager; @@ -1043,6 +1045,7 @@ 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 +1075,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 +1251,31 @@ 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); + var end = Math.Clamp(r.Start + r.Length, start, Editor.Document.TextLength); + if (end <= start) + return; + var mark = textMarkerService.Create(start, end - start); + mark.BackgroundColor = DebugStepBackground; + debugStepMarks.Add(mark); + Editor.TextArea.Caret.Offset = start; + Editor.TextArea.Caret.BringCaretToView(); + CaretHighlightAdorner.DisplayCaretHighlightAnimation(Editor.TextArea); + } + + + void ClearDebugStepMarks() + { + foreach (var mark in debugStepMarks) + textMarkerService.Remove(mark); + debugStepMarks.Clear(); + } + // Live cursor + full ScrollOffset, queried at hover-event time. Returns null if the // pointer is past the end of the line so we don't snap back to the last visual // column when the user is hovering empty trailing space. diff --git a/ILSpy/TextView/NodeLookup.cs b/ILSpy/TextView/NodeLookup.cs new file mode 100644 index 0000000000..b68f0f4ba2 --- /dev/null +++ b/ILSpy/TextView/NodeLookup.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; + +using ICSharpCode.Decompiler.CSharp.Syntax; + +namespace ICSharpCode.ILSpy.TextView +{ + /// + /// Maps syntax tree nodes to character ranges in the rendered text. + /// + internal sealed class NodeLookup + { + readonly Dictionary nodes = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRange(object node, out TextRange range) + => nodes.TryGetValue(node, out range); + + public void AddNode(object node, int start, int length) + { + if (length > 0) + { + nodes[node] = new TextRange(start, length); + if (node is IAnnotatable annotatable) + { + foreach (var annotation in annotatable.Annotations) + { + nodes[annotation] = new TextRange(start, length); + } + } + } + } + } +} diff --git a/ILSpy/TextView/TextRange.cs b/ILSpy/TextView/TextRange.cs new file mode 100644 index 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..b0dc7ff701 100644 --- a/ILSpy/ViewModels/DebugStepsPaneModel.cs +++ b/ILSpy/ViewModels/DebugStepsPaneModel.cs @@ -109,8 +109,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 +123,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); }); } @@ -222,12 +222,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); } } } From e46f6ac93d9d47911d5d758d07cb9987158737e6 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 27 Jun 2026 07:50:09 +0200 Subject: [PATCH 02/21] Center C# debug-step highlights Place the selected debug-step node in the middle of the editor after replay so navigation lands with enough surrounding context instead of just barely scrolling the mark into view. Assisted-by: CodeAlta:gpt-5.5:CodeAlta --- ILSpy/TextView/DecompilerTextView.axaml.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index 48b0e3d137..4a0e559145 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -1265,9 +1265,22 @@ void ApplyDebugStepHighlight(TextRange? range) debugStepMarks.Add(mark); Editor.TextArea.Caret.Offset = start; Editor.TextArea.Caret.BringCaretToView(); + Dispatcher.UIThread.Post(() => CenterDocumentOffsetInView(start)); CaretHighlightAdorner.DisplayCaretHighlightAnimation(Editor.TextArea); } + void CenterDocumentOffsetInView(int offset) + { + if (EditorScrollViewer is not { } scrollViewer || Editor.Document.TextLength == 0) + return; + offset = Math.Clamp(offset, 0, Editor.Document.TextLength); + var line = Editor.Document.GetLineByOffset(offset); + var lineHeight = Editor.TextArea.TextView.DefaultLineHeight; + if (lineHeight <= 0 || scrollViewer.Viewport.Height <= 0) + return; + var targetY = Math.Max(0, (line.LineNumber - 1) * lineHeight - (scrollViewer.Viewport.Height - lineHeight) / 2); + scrollViewer.Offset = new Vector(scrollViewer.Offset.X, targetY); + } void ClearDebugStepMarks() { From bbe2b6acef470562677d866942259b9c5c92144f Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 28 Jun 2026 06:52:25 +0200 Subject: [PATCH 03/21] Highlight and scroll to the changed instruction in the ILAst view The C# debug-steps view highlights and centers the exact AST node a transform changed; the ILAst view already had the step tree and replay-at-step but produced no highlight. Bring it to parity. IL rendering has no token-writer seam like the C# output visitor, so per-instruction text spans are recorded by bracketing ILInstruction.WriteTo via a new INodeTrackingOutput. The dominant inst.ReplaceWith(newInst) transform pattern detaches the instruction passed to Step, so ILTransformContext gains EndStep to record the produced instruction; Stepper additionally records the position's ancestor chain as fallback candidates before the step-limit throw, so the "show state before" view -- which halts at the selected step -- still resolves to a surviving ancestor (ultimately the ILFunction). The highlight-range resolver is shared with the C# language. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../Output/TextTokenWriterTests.cs | 2 +- ICSharpCode.Decompiler/IL/Instructions.cs | 805 +++++++++++------- ICSharpCode.Decompiler/IL/Instructions.tt | 7 +- .../Instructions/BinaryNumericInstruction.cs | 52 +- .../IL/Instructions/Block.cs | 58 +- .../IL/Instructions/BlockContainer.cs | 78 +- .../IL/Instructions/Branch.cs | 16 +- .../IL/Instructions/CallIndirect.cs | 40 +- .../IL/Instructions/CallInstruction.cs | 40 +- .../IL/Instructions/Comp.cs | 70 +- .../CompoundAssignmentInstruction.cs | 104 ++- .../IL/Instructions/Conv.cs | 76 +- .../IL/Instructions/DeconstructInstruction.cs | 56 +- .../DeconstructResultInstruction.cs | 22 +- .../IL/Instructions/DynamicInstructions.cs | 264 ++++-- .../IL/Instructions/ExpressionTreeCast.cs | 26 +- .../IL/Instructions/ILFunction.cs | 138 +-- .../IL/Instructions/IfInstruction.cs | 56 +- .../IL/Instructions/LdLen.cs | 22 +- .../IL/Instructions/Leave.cs | 24 +- .../IL/Instructions/LockInstruction.cs | 26 +- .../IL/Instructions/MatchInstruction.cs | 90 +- .../IL/Instructions/MemoryInstructions.cs | 94 +- .../Instructions/NullCoalescingInstruction.cs | 22 +- .../IL/Instructions/NullableInstructions.cs | 22 +- .../IL/Instructions/SimpleInstruction.cs | 74 +- .../IL/Instructions/StringToInt.cs | 40 +- .../IL/Instructions/SwitchInstruction.cs | 80 +- .../IL/Instructions/TryInstruction.cs | 86 +- .../IL/Instructions/UnaryInstruction.cs | 22 +- .../IL/Instructions/UsingInstruction.cs | 40 +- .../CachedDelegateInitialization.cs | 10 +- .../IL/Transforms/CopyPropagation.cs | 1 + .../IL/Transforms/DeconstructionTransform.cs | 1 + .../IL/Transforms/DelegateConstruction.cs | 4 + .../DetectCatchWhenConditionBlocks.cs | 4 +- .../DynamicIsEventAssignmentTransform.cs | 1 + .../Transforms/EarlyExpressionTransforms.cs | 5 + .../IL/Transforms/ExpressionTransforms.cs | 67 +- .../IL/Transforms/FixRemainingIncrements.cs | 13 +- .../IL/Transforms/IILTransform.cs | 19 + .../IL/Transforms/ILInlining.cs | 10 +- .../IL/Transforms/IndexRangeTransform.cs | 2 + .../IL/Transforms/InlineArrayTransform.cs | 12 +- .../Transforms/InterpolatedStringTransform.cs | 1 + .../Transforms/LdLocaDupInitObjTransform.cs | 4 +- .../IL/Transforms/LocalFunctionDecompiler.cs | 7 +- .../IL/Transforms/LockTransform.cs | 20 +- .../IL/Transforms/NamedArgumentTransform.cs | 4 +- .../IL/Transforms/NullPropagationTransform.cs | 19 +- .../IL/Transforms/NullableLiftingTransform.cs | 3 + .../IL/Transforms/PatternMatchingTransform.cs | 17 +- .../IL/Transforms/ProxyCallReplacer.cs | 1 + .../IL/Transforms/ReduceNestingTransform.cs | 12 +- .../IL/Transforms/RemoveDeadVariableInit.cs | 4 +- .../RemoveInfeasiblePathTransform.cs | 4 +- ...eUnconstrainedGenericReferenceTypeCheck.cs | 1 + .../IL/Transforms/Stepper.cs | 9 + .../IL/Transforms/SwitchOnStringTransform.cs | 6 + .../Transforms/TransformArrayInitializers.cs | 29 +- .../IL/Transforms/TransformAssignment.cs | 15 +- ...ransformCollectionAndObjectInitializers.cs | 1 + .../Transforms/TransformDisplayClassUsage.cs | 20 +- .../IL/Transforms/TransformExpressionTrees.cs | 1 + .../Transforms/UserDefinedLogicTransform.cs | 5 +- .../IL/Transforms/UsingTransform.cs | 12 +- .../Output/INodeTrackingOutput.cs | 59 ++ .../Output/TextTokenWriter.cs | 6 +- ILSpy.Tests/Views/DebugStepsTests.cs | 66 ++ ILSpy/Languages/CSharpLanguage.cs | 60 +- ILSpy/Languages/ILAstLanguage.cs | 5 + ILSpy/TextView/AvaloniaEditTextOutput.cs | 6 +- ILSpy/TextView/DebugStepHighlighter.cs | 73 ++ 73 files changed, 2077 insertions(+), 1094 deletions(-) create mode 100644 ICSharpCode.Decompiler/Output/INodeTrackingOutput.cs create mode 100644 ILSpy/TextView/DebugStepHighlighter.cs 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/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index 490bc19db0..b29c7e7456 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -372,11 +372,16 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } } } @@ -468,13 +473,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.left.WriteTo(output, options); - output.Write(", "); - this.right.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.left.WriteTo(output, options); + output.Write(", "); + this.right.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } } } @@ -626,13 +636,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.target.WriteTo(output, options); - output.Write(", "); - this.value.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.target.WriteTo(output, options); + output.Write(", "); + this.value.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } } } @@ -1011,15 +1026,20 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - variable.WriteTo(output); - output.Write('('); - this.init.WriteTo(output, options); - output.Write(", "); - this.body.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + variable.WriteTo(output); + output.Write('('); + this.init.WriteTo(output, options); + output.Write(", "); + this.body.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -2341,10 +2361,15 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - variable.WriteTo(output); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + variable.WriteTo(output); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -2415,10 +2440,15 @@ protected override void Disconnected() public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - variable.WriteTo(output); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + variable.WriteTo(output); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -2550,13 +2580,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - variable.WriteTo(output); - output.Write('('); - this.value.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + variable.WriteTo(output); + output.Write('('); + this.value.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -2655,13 +2690,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - this.value.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + this.value.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -2820,10 +2860,15 @@ public LdStr(string value) : base(OpCode.LdStr) public override StackType ResultType { get { return StackType.O; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -2857,10 +2902,15 @@ public LdStrUtf8(string value) : base(OpCode.LdStrUtf8) public override StackType ResultType { get { return StackType.O; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -2894,10 +2944,15 @@ public LdcI4(int value) : base(OpCode.LdcI4) public override StackType ResultType { get { return StackType.I4; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -2931,10 +2986,15 @@ public LdcI8(long value) : base(OpCode.LdcI8) public override StackType ResultType { get { return StackType.I8; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -2968,10 +3028,15 @@ public LdcF4(float value) : base(OpCode.LdcF4) public override StackType ResultType { get { return StackType.F4; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3005,10 +3070,15 @@ public LdcF8(double value) : base(OpCode.LdcF8) public override StackType ResultType { get { return StackType.F8; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3042,10 +3112,15 @@ public LdcDecimal(decimal value) : base(OpCode.LdcDecimal) public override StackType ResultType { get { return StackType.O; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3109,13 +3184,18 @@ public LdFtn(IMethod method) : base(OpCode.LdFtn) public override StackType ResultType { get { return StackType.I; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (method != null) + output.MarkNodeStart(this); + try { - output.Write(' '); - method.WriteTo(output); + WriteILRange(output, options); + output.Write(OpCode); + if (method != null) + { + output.Write(' '); + method.WriteTo(output); + } } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3160,16 +3240,21 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (method != null) + output.MarkNodeStart(this); + try { - output.Write(' '); - method.WriteTo(output); + WriteILRange(output, options); + output.Write(OpCode); + if (method != null) + { + output.Write(' '); + method.WriteTo(output); + } + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3221,18 +3306,23 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - if (method != null) + output.MarkNodeStart(this); + try { + WriteILRange(output, options); + output.Write(OpCode); output.Write(' '); - method.WriteTo(output); - } - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + type.WriteTo(output); + if (method != null) + { + output.Write(' '); + method.WriteTo(output); + } + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3271,10 +3361,15 @@ public IType Type { public override StackType ResultType { get { return StackType.O; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3310,10 +3405,15 @@ public LdMemberToken(IMember member) : base(OpCode.LdMemberToken) public override StackType ResultType { get { return StackType.O; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - member.WriteTo(output); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + member.WriteTo(output); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3398,13 +3498,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3536,19 +3641,24 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - if (IsVolatile) - output.Write("volatile."); - if (UnalignedPrefix > 0) - output.Write("unaligned(" + UnalignedPrefix + ")."); - output.Write(OpCode); - output.Write('('); - this.destAddress.WriteTo(output, options); - output.Write(", "); - this.sourceAddress.WriteTo(output, options); - output.Write(", "); - this.size.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + if (IsVolatile) + output.Write("volatile."); + if (UnalignedPrefix > 0) + output.Write("unaligned(" + UnalignedPrefix + ")."); + output.Write(OpCode); + output.Write('('); + this.destAddress.WriteTo(output, options); + output.Write(", "); + this.sourceAddress.WriteTo(output, options); + output.Write(", "); + this.size.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3687,19 +3797,24 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - if (IsVolatile) - output.Write("volatile."); - if (UnalignedPrefix > 0) - output.Write("unaligned(" + UnalignedPrefix + ")."); - output.Write(OpCode); - output.Write('('); - this.address.WriteTo(output, options); - output.Write(", "); - this.value.WriteTo(output, options); - output.Write(", "); - this.size.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + if (IsVolatile) + output.Write("volatile."); + if (UnalignedPrefix > 0) + output.Write("unaligned(" + UnalignedPrefix + ")."); + output.Write(OpCode); + output.Write('('); + this.address.WriteTo(output, options); + output.Write(", "); + this.value.WriteTo(output, options); + output.Write(", "); + this.size.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3803,15 +3918,20 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - if (DelayExceptions) - output.Write("delayex."); - output.Write(OpCode); - output.Write(' '); - @field.WriteTo(output); - output.Write('('); - this.target.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + if (DelayExceptions) + output.Write("delayex."); + output.Write(OpCode); + output.Write(' '); + @field.WriteTo(output); + output.Write('('); + this.target.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3847,10 +3967,15 @@ public LdsFlda(IField @field) : base(OpCode.LdsFlda) public IField Field { get { return @field; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - @field.WriteTo(output); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + @field.WriteTo(output); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3898,13 +4023,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3943,13 +4073,18 @@ public IType Type { public override StackType ResultType { get { return StackType.O; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -4166,13 +4301,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - this.target.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + this.target.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -4357,13 +4497,18 @@ public IType Type { public override StackType ResultType { get { return StackType.O; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -4411,13 +4556,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -4465,13 +4615,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -4582,21 +4737,26 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - bool first = true; - foreach (var indices in Indices) + output.MarkNodeStart(this); + try { - if (!first) - output.Write(", "); - else - first = false; - indices.WriteTo(output, options); - } - output.Write(')'); + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + bool first = true; + foreach (var indices in Indices) + { + if (!first) + output.Write(", "); + else + first = false; + indices.WriteTo(output, options); + } + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -4635,10 +4795,15 @@ public IType Type { public override StackType ResultType { get { return type.GetStackType(); } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -4751,10 +4916,15 @@ public IType Type { public override StackType ResultType { get { return StackType.I4; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -4952,24 +5122,29 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - if (WithSystemIndex) - output.Write("withsystemindex."); - if (DelayExceptions) - output.Write("delayex."); - if (IsReadOnly) - output.Write("readonly."); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - this.array.WriteTo(output, options); - foreach (var indices in Indices) - { - output.Write(", "); - indices.WriteTo(output, options); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + if (WithSystemIndex) + output.Write("withsystemindex."); + if (DelayExceptions) + output.Write("delayex."); + if (IsReadOnly) + output.Write("readonly."); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + this.array.WriteTo(output, options); + foreach (var indices in Indices) + { + output.Write(", "); + indices.WriteTo(output, options); + } + output.Write(')'); } - output.Write(')'); + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -5077,20 +5252,25 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - if (IsReadOnly) - output.Write("readonly."); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - this.array.WriteTo(output, options); - foreach (var indices in Indices) + output.MarkNodeStart(this); + try { - output.Write(", "); - indices.WriteTo(output, options); + WriteILRange(output, options); + if (IsReadOnly) + output.Write("readonly."); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + this.array.WriteTo(output, options); + foreach (var indices in Indices) + { + output.Write(", "); + indices.WriteTo(output, options); + } + output.Write(')'); } - output.Write(')'); + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -5190,16 +5370,21 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (method != null) + output.MarkNodeStart(this); + try { - output.Write(' '); - method.WriteTo(output); + WriteILRange(output, options); + output.Write(OpCode); + if (method != null) + { + output.Write(' '); + method.WriteTo(output); + } + output.Write('('); + this.argument.WriteTo(output, options); + output.Write(')'); } - output.Write('('); - this.argument.WriteTo(output, options); - output.Write(')'); + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -5438,18 +5623,23 @@ public sealed override ILInstruction Clone() } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (method != null) - { - output.Write(' '); - method.WriteTo(output); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + if (method != null) + { + output.Write(' '); + method.WriteTo(output); + } + output.Write('('); + this.left.WriteTo(output, options); + output.Write(", "); + this.right.WriteTo(output, options); + output.Write(')'); } - output.Write('('); - this.left.WriteTo(output, options); - output.Write(", "); - this.right.WriteTo(output, options); - output.Write(')'); + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -6635,13 +6825,18 @@ public IType Type { public override StackType ResultType { get { return StackType.O; } } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -6717,13 +6912,18 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -6815,11 +7015,16 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.value.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.value.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -6911,11 +7116,16 @@ public override InstructionFlags DirectFlags { } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.value.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.value.WriteTo(output, options); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -7031,10 +7241,15 @@ public sealed override ILInstruction Clone() } public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + output.Write(')'); + } + finally { output.MarkNodeEnd(this); } } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index 74963251be..1035722357 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -427,10 +427,15 @@ 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) + { + output.MarkNodeStart(this); + try {<#=Body(opCode.WriteToBody)#>} + finally { output.MarkNodeEnd(this); } + } <# } #> - {<#=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..ca36302f03 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs @@ -186,32 +186,40 @@ internal static string GetOperatorName(BinaryNumericOperator @operator) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write("." + GetOperatorName(Operator)); - if (CheckForOverflow) + output.MarkNodeStart(this); + try { - output.Write(".ovf"); - } - if (Sign == Sign.Unsigned) - { - output.Write(".unsigned"); - } - else if (Sign == Sign.Signed) - { - output.Write(".signed"); + WriteILRange(output, options); + output.Write(OpCode); + output.Write("." + GetOperatorName(Operator)); + if (CheckForOverflow) + { + output.Write(".ovf"); + } + if (Sign == Sign.Unsigned) + { + output.Write(".unsigned"); + } + else if (Sign == Sign.Signed) + { + output.Write(".signed"); + } + output.Write('.'); + output.Write(resultType.ToString().ToLowerInvariant()); + if (IsLifted) + { + output.Write(".lifted"); + } + output.Write('('); + Left.WriteTo(output, options); + output.Write(", "); + Right.WriteTo(output, options); + output.Write(')'); } - output.Write('.'); - output.Write(resultType.ToString().ToLowerInvariant()); - if (IsLifted) + finally { - output.Write(".lifted"); + output.MarkNodeEnd(this); } - output.Write('('); - Left.WriteTo(output, options); - output.Write(", "); - Right.WriteTo(output, options); - output.Write(')'); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/Block.cs b/ICSharpCode.Decompiler/IL/Instructions/Block.cs index 2b0b3991e6..c6aae0f1bd 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Block.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Block.cs @@ -260,37 +260,45 @@ public string Label { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write("Block "); - output.WriteLocalReference(Label, this, isDefinition: true); - if (Kind != BlockKind.ControlFlow) - output.Write($" ({Kind})"); - if (Parent is BlockContainer) - output.Write(" (incoming: {0})", IncomingEdgeCount); - output.Write(' '); - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - int index = 0; - foreach (var inst in Instructions) + output.MarkNodeStart(this); + try { - if (options.ShowChildIndexInBlock) + WriteILRange(output, options); + output.Write("Block "); + output.WriteLocalReference(Label, this, isDefinition: true); + if (Kind != BlockKind.ControlFlow) + output.Write($" ({Kind})"); + if (Parent is BlockContainer) + output.Write(" (incoming: {0})", IncomingEdgeCount); + output.Write(' '); + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + int index = 0; + foreach (var inst in Instructions) + { + if (options.ShowChildIndexInBlock) + { + output.Write("[" + index + "] "); + index++; + } + inst.WriteTo(output, options); + output.WriteLine(); + } + if (finalInstruction.OpCode != OpCode.Nop) { - output.Write("[" + index + "] "); - index++; + output.Write("final: "); + finalInstruction.WriteTo(output, options); + output.WriteLine(); } - inst.WriteTo(output, options); - output.WriteLine(); + output.Unindent(); + output.Write("}"); + output.MarkFoldEnd(); } - if (finalInstruction.OpCode != OpCode.Nop) + finally { - output.Write("final: "); - finalInstruction.WriteTo(output, options); - output.WriteLine(); + output.MarkNodeEnd(this); } - output.Unindent(); - output.Write("}"); - output.MarkFoldEnd(); } protected override int GetChildCount() diff --git a/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs b/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs index 131bf5f3b4..fc341879b4 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs @@ -127,47 +127,55 @@ protected override void Disconnected() public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.WriteLocalReference("BlockContainer", this, isDefinition: true); - output.Write(' '); - switch (Kind) - { - case ContainerKind.Loop: - output.Write("(while-true) "); - break; - case ContainerKind.Switch: - output.Write("(switch) "); - break; - case ContainerKind.While: - output.Write("(while) "); - break; - case ContainerKind.DoWhile: - output.Write("(do-while) "); - break; - case ContainerKind.For: - output.Write("(for) "); - break; - } - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - foreach (var inst in Blocks) + output.MarkNodeStart(this); + try { - if (inst.Parent == this) + WriteILRange(output, options); + output.WriteLocalReference("BlockContainer", this, isDefinition: true); + output.Write(' '); + switch (Kind) { - inst.WriteTo(output, options); + case ContainerKind.Loop: + output.Write("(while-true) "); + break; + case ContainerKind.Switch: + output.Write("(switch) "); + break; + case ContainerKind.While: + output.Write("(while) "); + break; + case ContainerKind.DoWhile: + output.Write("(do-while) "); + break; + case ContainerKind.For: + output.Write("(for) "); + break; } - else + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + foreach (var inst in Blocks) { - output.Write("stale reference to "); - output.WriteLocalReference(inst.Label, inst); + if (inst.Parent == this) + { + inst.WriteTo(output, options); + } + else + { + output.Write("stale reference to "); + output.WriteLocalReference(inst.Label, inst); + } + output.WriteLine(); + output.WriteLine(); } - output.WriteLine(); - output.WriteLine(); + output.Unindent(); + output.Write("}"); + output.MarkFoldEnd(); + } + finally + { + output.MarkNodeEnd(this); } - output.Unindent(); - output.Write("}"); - output.MarkFoldEnd(); } protected override int GetChildCount() diff --git a/ICSharpCode.Decompiler/IL/Instructions/Branch.cs b/ICSharpCode.Decompiler/IL/Instructions/Branch.cs index bab8b9fc06..a398d92617 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Branch.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Branch.cs @@ -120,10 +120,18 @@ internal override void CheckInvariant(ILPhase phase) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - output.WriteLocalReference(TargetLabel, (object?)targetBlock ?? TargetILOffset); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + output.WriteLocalReference(TargetLabel, (object?)targetBlock ?? TargetILOffset); + } + finally + { + output.MarkNodeEnd(this); + } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs b/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs index e165f5df02..d4be2caf80 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs @@ -76,26 +76,34 @@ internal override void CheckInvariant(ILPhase phase) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write("call.indirect "); - FunctionPointerType.ReturnType.WriteTo(output); - output.Write('('); - functionPointer.WriteTo(output, options); - int firstArgument = IsInstance ? 1 : 0; - if (firstArgument == 1) + output.MarkNodeStart(this); + try { - output.Write(", "); - Arguments[0].WriteTo(output, options); + WriteILRange(output, options); + output.Write("call.indirect "); + FunctionPointerType.ReturnType.WriteTo(output); + output.Write('('); + functionPointer.WriteTo(output, options); + int firstArgument = IsInstance ? 1 : 0; + if (firstArgument == 1) + { + output.Write(", "); + Arguments[0].WriteTo(output, options); + } + foreach (var (inst, type) in Arguments.Zip(FunctionPointerType.ParameterTypes, (a, b) => (a, b))) + { + output.Write(", "); + inst.WriteTo(output, options); + output.Write(" : "); + type.WriteTo(output); + } + if (Arguments.Count > 0) + output.Write(')'); } - foreach (var (inst, type) in Arguments.Zip(FunctionPointerType.ParameterTypes, (a, b) => (a, b))) + finally { - output.Write(", "); - inst.WriteTo(output, options); - output.Write(" : "); - type.WriteTo(output); + output.MarkNodeEnd(this); } - if (Arguments.Count > 0) - output.Write(')'); } protected override int GetChildCount() diff --git a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs index a09aa66212..0a1025ff33 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs @@ -140,26 +140,34 @@ internal override void CheckInvariant(ILPhase phase) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - if (ConstrainedTo != null) + output.MarkNodeStart(this); + try { - output.Write("constrained["); - ConstrainedTo.WriteTo(output); - output.Write("]."); + WriteILRange(output, options); + if (ConstrainedTo != null) + { + output.Write("constrained["); + ConstrainedTo.WriteTo(output); + output.Write("]."); + } + if (IsTail) + output.Write("tail."); + output.Write(OpCode); + output.Write(' '); + Method.WriteTo(output); + output.Write('('); + for (int i = 0; i < Arguments.Count; i++) + { + if (i > 0) + output.Write(", "); + Arguments[i].WriteTo(output, options); + } + output.Write(')'); } - if (IsTail) - output.Write("tail."); - output.Write(OpCode); - output.Write(' '); - Method.WriteTo(output); - output.Write('('); - for (int i = 0; i < Arguments.Count; i++) + finally { - if (i > 0) - output.Write(", "); - Arguments[i].WriteTo(output, options); + output.MarkNodeEnd(this); } - output.Write(')'); } protected internal sealed override bool PerformMatch(ILInstruction? other, ref Patterns.Match match) diff --git a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs index c7e26ff459..8e98e002ee 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs @@ -181,42 +181,50 @@ internal override void CheckInvariant(ILPhase phase) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - if (options.UseLogicOperationSugar && MatchLogicNot(out var arg)) + output.MarkNodeStart(this); + try { - output.Write("logic.not("); - arg.WriteTo(output, options); + WriteILRange(output, options); + if (options.UseLogicOperationSugar && MatchLogicNot(out var arg)) + { + output.Write("logic.not("); + arg.WriteTo(output, options); + output.Write(')'); + return; + } + output.Write(OpCode); + output.Write('.'); + output.Write(InputType.ToString().ToLower()); + switch (Sign) + { + case Sign.Signed: + output.Write(".signed"); + break; + case Sign.Unsigned: + output.Write(".unsigned"); + break; + } + switch (LiftingKind) + { + case ComparisonLiftingKind.CSharp: + output.Write(".lifted[C#]"); + break; + case ComparisonLiftingKind.ThreeValuedLogic: + output.Write(".lifted[3VL]"); + break; + } + output.Write('('); + Left.WriteTo(output, options); + output.Write(' '); + output.Write(Kind.GetToken()); + output.Write(' '); + Right.WriteTo(output, options); output.Write(')'); - return; } - output.Write(OpCode); - output.Write('.'); - output.Write(InputType.ToString().ToLower()); - switch (Sign) + finally { - case Sign.Signed: - output.Write(".signed"); - break; - case Sign.Unsigned: - output.Write(".unsigned"); - break; + output.MarkNodeEnd(this); } - switch (LiftingKind) - { - case ComparisonLiftingKind.CSharp: - output.Write(".lifted[C#]"); - break; - case ComparisonLiftingKind.ThreeValuedLogic: - output.Write(".lifted[3VL]"); - break; - } - output.Write('('); - Left.WriteTo(output, options); - output.Write(' '); - output.Write(Kind.GetToken()); - output.Write(' '); - Right.WriteTo(output, options); - output.Write(')'); } public static Comp LogicNot(ILInstruction arg) diff --git a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs index eab1cbb290..f6cecb8d14 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs @@ -274,33 +274,41 @@ public override InstructionFlags DirectFlags { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write("." + BinaryNumericInstruction.GetOperatorName(Operator)); - if (CheckForOverflow) + output.MarkNodeStart(this); + try { - output.Write(".ovf"); - } - if (Sign == Sign.Unsigned) - { - output.Write(".unsigned"); - } - else if (Sign == Sign.Signed) - { - output.Write(".signed"); + WriteILRange(output, options); + output.Write(OpCode); + output.Write("." + BinaryNumericInstruction.GetOperatorName(Operator)); + if (CheckForOverflow) + { + output.Write(".ovf"); + } + if (Sign == Sign.Unsigned) + { + output.Write(".unsigned"); + } + else if (Sign == Sign.Signed) + { + output.Write(".signed"); + } + output.Write('.'); + output.Write(UnderlyingResultType.ToString().ToLowerInvariant()); + if (IsLifted) + { + output.Write(".lifted"); + } + base.WriteSuffix(output); + output.Write('('); + Target.WriteTo(output, options); + output.Write(", "); + Value.WriteTo(output, options); + output.Write(')'); } - output.Write('.'); - output.Write(UnderlyingResultType.ToString().ToLowerInvariant()); - if (IsLifted) + finally { - output.Write(".lifted"); + output.MarkNodeEnd(this); } - base.WriteSuffix(output); - output.Write('('); - Target.WriteTo(output, options); - output.Write(", "); - Value.WriteTo(output, options); - output.Write(')'); } } @@ -338,16 +346,24 @@ public static bool IsStringConcat(IMethod method) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - base.WriteSuffix(output); - output.Write(' '); - Method.WriteTo(output); - output.Write('('); - this.Target.WriteTo(output, options); - output.Write(", "); - this.Value.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + base.WriteSuffix(output); + output.Write(' '); + Method.WriteTo(output); + output.Write('('); + this.Target.WriteTo(output, options); + output.Write(", "); + this.Value.WriteTo(output, options); + output.Write(')'); + } + finally + { + output.MarkNodeEnd(this); + } } } @@ -374,13 +390,21 @@ public DynamicCompoundAssign(ExpressionType op, CSharpBinderFlags binderFlags, public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write("." + Operation.ToString().ToLower()); - DynamicInstruction.WriteBinderFlags(BinderFlags, output, options); - base.WriteSuffix(output); - output.Write(' '); - DynamicInstruction.WriteArgumentList(output, options, (Target, TargetArgumentInfo), (Value, ValueArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write("." + Operation.ToString().ToLower()); + DynamicInstruction.WriteBinderFlags(BinderFlags, output, options); + base.WriteSuffix(output); + output.Write(' '); + DynamicInstruction.WriteArgumentList(output, options, (Target, TargetArgumentInfo), (Value, ValueArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } internal static bool IsExpressionTypeSupported(ExpressionType type) diff --git a/ICSharpCode.Decompiler/IL/Instructions/Conv.cs b/ICSharpCode.Decompiler/IL/Instructions/Conv.cs index e2ae200c9a..d5fe091fd2 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Conv.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Conv.cs @@ -316,44 +316,52 @@ public StackType UnderlyingResultType { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (CheckForOverflow) - { - output.Write(".ovf"); - } - if (InputSign == Sign.Unsigned) - { - output.Write(".unsigned"); - } - else if (InputSign == Sign.Signed) - { - output.Write(".signed"); - } - if (IsLifted) + output.MarkNodeStart(this); + try { - output.Write(".lifted"); + WriteILRange(output, options); + output.Write(OpCode); + if (CheckForOverflow) + { + output.Write(".ovf"); + } + if (InputSign == Sign.Unsigned) + { + output.Write(".unsigned"); + } + else if (InputSign == Sign.Signed) + { + output.Write(".signed"); + } + if (IsLifted) + { + output.Write(".lifted"); + } + output.Write(' '); + output.Write(InputType); + output.Write("->"); + output.Write(TargetType); + output.Write(' '); + switch (Kind) + { + case ConversionKind.SignExtend: + output.Write(""); + break; + case ConversionKind.ZeroExtend: + output.Write(""); + break; + case ConversionKind.Invalid: + output.Write(""); + break; + } + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } - output.Write(' '); - output.Write(InputType); - output.Write("->"); - output.Write(TargetType); - output.Write(' '); - switch (Kind) + finally { - case ConversionKind.SignExtend: - output.Write(""); - break; - case ConversionKind.ZeroExtend: - output.Write(""); - break; - case ConversionKind.Invalid: - output.Write(""); - break; + output.MarkNodeEnd(this); } - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); } protected override InstructionFlags ComputeFlags() diff --git a/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs index 4a4b0e1b2e..d02f1824c7 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs @@ -161,33 +161,41 @@ protected internal override void InstructionCollectionUpdateComplete() public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write("deconstruct "); - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - output.WriteLine("init:"); - output.Indent(); - foreach (var inst in this.Init) + output.MarkNodeStart(this); + try { - inst.WriteTo(output, options); + WriteILRange(output, options); + output.Write("deconstruct "); + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + output.WriteLine("init:"); + output.Indent(); + foreach (var inst in this.Init) + { + inst.WriteTo(output, options); + output.WriteLine(); + } + output.Unindent(); + output.WriteLine("pattern:"); + output.Indent(); + pattern.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write("conversions: "); + conversions.WriteTo(output, options); output.WriteLine(); + output.Write("assignments: "); + assignments.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write('}'); + output.MarkFoldEnd(); + } + finally + { + output.MarkNodeEnd(this); } - output.Unindent(); - output.WriteLine("pattern:"); - output.Indent(); - pattern.WriteTo(output, options); - output.Unindent(); - output.WriteLine(); - output.Write("conversions: "); - conversions.WriteTo(output, options); - output.WriteLine(); - output.Write("assignments: "); - assignments.WriteTo(output, options); - output.Unindent(); - output.WriteLine(); - output.Write('}'); - output.MarkFoldEnd(); } internal static bool IsConversionStLoc(ILInstruction inst, out ILVariable variable, out ILVariable inputVariable) diff --git a/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs index 358b761efa..f3f1718154 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs @@ -39,13 +39,21 @@ public DeconstructResultInstruction(int index, StackType resultType, ILInstructi public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - output.Write(Index.ToString()); - output.Write('('); - this.Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + output.Write(Index.ToString()); + output.Write('('); + this.Argument.WriteTo(output, options); + output.Write(')'); + } + finally + { + output.MarkNodeEnd(this); + } } MatchInstruction? FindMatch() diff --git a/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs b/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs index ee878ebb48..568a315170 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs @@ -132,14 +132,22 @@ partial class DynamicConvertInstruction { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + argument.WriteTo(output, options); + output.Write(')'); + } + finally + { + output.MarkNodeEnd(this); + } } public DynamicConvertInstruction(CSharpBinderFlags binderFlags, IType type, IType? context, ILInstruction argument) @@ -184,25 +192,33 @@ public DynamicInvokeMemberInstruction(CSharpBinderFlags binderFlags, string name public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Name); - if (TypeArguments.Count > 0) + output.MarkNodeStart(this); + try { - output.Write('<'); - int i = 0; - foreach (var typeArg in TypeArguments) + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Name); + if (TypeArguments.Count > 0) { - if (i > 0) - output.Write(", "); - typeArg.WriteTo(output); - i++; + output.Write('<'); + int i = 0; + foreach (var typeArg in TypeArguments) + { + if (i > 0) + output.Write(", "); + typeArg.WriteTo(output); + i++; + } + output.Write('>'); } - output.Write('>'); + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); } - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); } public override StackType ResultType => StackType.O; @@ -230,12 +246,20 @@ public DynamicGetMemberInstruction(CSharpBinderFlags binderFlags, string? name, public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Name); - WriteArgumentList(output, options, (Target, TargetArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Name); + WriteArgumentList(output, options, (Target, TargetArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType => StackType.O; @@ -266,12 +290,20 @@ public DynamicSetMemberInstruction(CSharpBinderFlags binderFlags, string? name, public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Name); - WriteArgumentList(output, options, (Target, TargetArgumentInfo), (Value, ValueArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Name); + WriteArgumentList(output, options, (Target, TargetArgumentInfo), (Value, ValueArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType => StackType.O; @@ -304,12 +336,20 @@ public DynamicGetIndexInstruction(CSharpBinderFlags binderFlags, IType? context, public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write("get_Item"); - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write("get_Item"); + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType => StackType.O; @@ -336,12 +376,20 @@ public DynamicSetIndexInstruction(CSharpBinderFlags binderFlags, IType? context, public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write("set_Item"); - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write("set_Item"); + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType => StackType.O; @@ -371,13 +419,21 @@ public DynamicInvokeConstructorInstruction(CSharpBinderFlags binderFlags, IType? public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - resultType?.WriteTo(output); - output.Write(".ctor"); - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + resultType?.WriteTo(output); + output.Write(".ctor"); + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType => resultType?.GetStackType() ?? StackType.Unknown; @@ -408,12 +464,20 @@ public DynamicBinaryOperatorInstruction(CSharpBinderFlags binderFlags, Expressio public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Operation.ToString()); - WriteArgumentList(output, options, (Left, LeftArgumentInfo), (Right, RightArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Operation.ToString()); + WriteArgumentList(output, options, (Left, LeftArgumentInfo), (Right, RightArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType => StackType.O; @@ -450,12 +514,20 @@ public DynamicLogicOperatorInstruction(CSharpBinderFlags binderFlags, Expression public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Operation.ToString()); - WriteArgumentList(output, options, (Left, LeftArgumentInfo), (Right, RightArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Operation.ToString()); + WriteArgumentList(output, options, (Left, LeftArgumentInfo), (Right, RightArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType => StackType.O; @@ -497,12 +569,20 @@ public DynamicUnaryOperatorInstruction(CSharpBinderFlags binderFlags, Expression public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Operation.ToString()); - WriteArgumentList(output, options, (Operand, OperandArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Operation.ToString()); + WriteArgumentList(output, options, (Operand, OperandArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType { @@ -544,11 +624,19 @@ public DynamicInvokeInstruction(CSharpBinderFlags binderFlags, IType? context, C public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType => StackType.O; @@ -574,13 +662,21 @@ public DynamicIsEventInstruction(CSharpBinderFlags binderFlags, string? name, IT public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType => StackType.I4; diff --git a/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs b/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs index b3dc073683..4434ae5caf 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs @@ -16,15 +16,23 @@ public ExpressionTreeCast(IType type, ILInstruction argument, bool isChecked) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (IsChecked) - output.Write(".checked"); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + if (IsChecked) + output.Write(".checked"); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally + { + output.MarkNodeEnd(this); + } } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs b/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs index eb81ccbcea..97e601dd93 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs @@ -272,83 +272,91 @@ void CloneVariables() public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (Method != null) + output.MarkNodeStart(this); + try { - output.Write(' '); - Method.WriteTo(output); - } - switch (kind) - { - case ILFunctionKind.ExpressionTree: - output.Write(".ET"); - break; - case ILFunctionKind.LocalFunction: - output.Write(".local"); - break; - } - if (DelegateType != null) - { - output.Write("["); - DelegateType.WriteTo(output); - output.Write("]"); - } - output.WriteLine(" {"); - output.Indent(); + WriteILRange(output, options); + output.Write(OpCode); + if (Method != null) + { + output.Write(' '); + Method.WriteTo(output); + } + switch (kind) + { + case ILFunctionKind.ExpressionTree: + output.Write(".ET"); + break; + case ILFunctionKind.LocalFunction: + output.Write(".local"); + break; + } + if (DelegateType != null) + { + output.Write("["); + DelegateType.WriteTo(output); + output.Write("]"); + } + output.WriteLine(" {"); + output.Indent(); - if (IsAsync) - { - output.WriteLine(".async"); - } - if (IsIterator) - { - output.WriteLine(".iterator"); - } - if (DeclarationScope != null) - { - output.Write("declared as " + Name + " in "); - output.WriteLocalReference(DeclarationScope.EntryPoint.Label, DeclarationScope); - output.WriteLine(); - } + if (IsAsync) + { + output.WriteLine(".async"); + } + if (IsIterator) + { + output.WriteLine(".iterator"); + } + if (DeclarationScope != null) + { + output.Write("declared as " + Name + " in "); + output.WriteLocalReference(DeclarationScope.EntryPoint.Label, DeclarationScope); + output.WriteLine(); + } - output.MarkFoldStart(Variables.Count + " variable(s)", true); - foreach (var variable in Variables) - { - variable.WriteDefinitionTo(output); + output.MarkFoldStart(Variables.Count + " variable(s)", true); + foreach (var variable in Variables) + { + variable.WriteDefinitionTo(output); + output.WriteLine(); + } + output.MarkFoldEnd(); output.WriteLine(); - } - output.MarkFoldEnd(); - output.WriteLine(); - - foreach (string warning in Warnings) - { - output.WriteLine("//" + warning); - } - body.WriteTo(output, options); - output.WriteLine(); + foreach (string warning in Warnings) + { + output.WriteLine("//" + warning); + } - foreach (var localFunction in LocalFunctions) - { + body.WriteTo(output, options); output.WriteLine(); - localFunction.WriteTo(output, options); - } - if (options.ShowILRanges) - { - var unusedILRanges = FindUnusedILRanges(); - if (!unusedILRanges.IsEmpty) + foreach (var localFunction in LocalFunctions) { - output.Write("// Unused IL Ranges: "); - output.Write(string.Join(", ", unusedILRanges.Intervals.Select( - range => $"[{range.Start:x4}..{range.InclusiveEnd:x4}]"))); output.WriteLine(); + localFunction.WriteTo(output, options); } - } - output.Unindent(); - output.WriteLine("}"); + if (options.ShowILRanges) + { + var unusedILRanges = FindUnusedILRanges(); + if (!unusedILRanges.IsEmpty) + { + output.Write("// Unused IL Ranges: "); + output.Write(string.Join(", ", unusedILRanges.Intervals.Select( + range => $"[{range.Start:x4}..{range.InclusiveEnd:x4}]"))); + output.WriteLine(); + } + } + + output.Unindent(); + output.WriteLine("}"); + } + finally + { + output.MarkNodeEnd(this); + } } LongSet FindUnusedILRanges() diff --git a/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs index 3cd3caf931..7328beee06 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs @@ -84,37 +84,45 @@ protected override InstructionFlags ComputeFlags() public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - if (options.UseLogicOperationSugar) + output.MarkNodeStart(this); + try { - if (MatchLogicAnd(out var lhs, out var rhs)) + WriteILRange(output, options); + if (options.UseLogicOperationSugar) { - output.Write("logic.and("); - lhs.WriteTo(output, options); - output.Write(", "); - rhs.WriteTo(output, options); - output.Write(')'); - return; + if (MatchLogicAnd(out var lhs, out var rhs)) + { + output.Write("logic.and("); + lhs.WriteTo(output, options); + output.Write(", "); + rhs.WriteTo(output, options); + output.Write(')'); + return; + } + if (MatchLogicOr(out lhs, out rhs)) + { + output.Write("logic.or("); + lhs.WriteTo(output, options); + output.Write(", "); + rhs.WriteTo(output, options); + output.Write(')'); + return; + } } - if (MatchLogicOr(out lhs, out rhs)) + output.Write(OpCode); + output.Write(" ("); + condition.WriteTo(output, options); + output.Write(") "); + trueInst.WriteTo(output, options); + if (falseInst.OpCode != OpCode.Nop) { - output.Write("logic.or("); - lhs.WriteTo(output, options); - output.Write(", "); - rhs.WriteTo(output, options); - output.Write(')'); - return; + output.Write(" else "); + falseInst.WriteTo(output, options); } } - output.Write(OpCode); - output.Write(" ("); - condition.WriteTo(output, options); - output.Write(") "); - trueInst.WriteTo(output, options); - if (falseInst.OpCode != OpCode.Nop) + finally { - output.Write(" else "); - falseInst.WriteTo(output, options); + output.MarkNodeEnd(this); } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs b/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs index 844552b2cf..f52c4a20d1 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs @@ -41,13 +41,21 @@ public override StackType ResultType { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('.'); - output.Write(resultType); - output.Write('('); - this.array.WriteTo(output, options); - output.Write(')'); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write('.'); + output.Write(resultType); + output.Write('('); + this.array.WriteTo(output, options); + output.Write(')'); + } + finally + { + output.MarkNodeEnd(this); + } } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/Leave.cs b/ICSharpCode.Decompiler/IL/Instructions/Leave.cs index bb1bc7de30..bd426503f4 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Leave.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Leave.cs @@ -116,15 +116,23 @@ internal override void CheckInvariant(ILPhase phase) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (targetContainer != null) + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + if (targetContainer != null) + { + output.Write(' '); + output.WriteLocalReference(TargetLabel, targetContainer); + output.Write(" ("); + value.WriteTo(output, options); + output.Write(')'); + } + } + finally { - output.Write(' '); - output.WriteLocalReference(TargetLabel, targetContainer); - output.Write(" ("); - value.WriteTo(output, options); - output.Write(')'); + output.MarkNodeEnd(this); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs index deb89f0a3f..29d9c5f0a1 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs @@ -29,15 +29,23 @@ partial class LockInstruction { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write("lock ("); - OnExpression.WriteTo(output, options); - output.WriteLine(") {"); - output.Indent(); - Body.WriteTo(output, options); - output.Unindent(); - output.WriteLine(); - output.Write("}"); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write("lock ("); + OnExpression.WriteTo(output, options); + output.WriteLine(") {"); + output.Indent(); + Body.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write("}"); + } + finally + { + output.MarkNodeEnd(this); + } } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs index 6939764afa..ede8f0569a 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs @@ -276,50 +276,58 @@ internal static bool IsDeconstructMethod(IMethod? method) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (CheckNotNull) + output.MarkNodeStart(this); + try { - output.Write(".notnull"); - } - if (CheckType) - { - output.Write(".type["); - variable.Type.WriteTo(output); - output.Write(']'); - } - if (IsDeconstructCall) - { - output.Write(".deconstruct["); - if (method == null) - output.Write(""); - else - method.WriteTo(output); - output.Write(']'); - } - if (IsDeconstructTuple) - { - output.Write(".tuple"); - } - output.Write(' '); - output.Write('('); - Variable.WriteTo(output); - output.Write(" = "); - TestedOperand.WriteTo(output, options); - output.Write(')'); - if (SubPatterns.Count > 0) - { - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - foreach (var pattern in SubPatterns) + WriteILRange(output, options); + output.Write(OpCode); + if (CheckNotNull) { - pattern.WriteTo(output, options); - output.WriteLine(); + output.Write(".notnull"); } - output.Unindent(); - output.Write('}'); - output.MarkFoldEnd(); + if (CheckType) + { + output.Write(".type["); + variable.Type.WriteTo(output); + output.Write(']'); + } + if (IsDeconstructCall) + { + output.Write(".deconstruct["); + if (method == null) + output.Write(""); + else + method.WriteTo(output); + output.Write(']'); + } + if (IsDeconstructTuple) + { + output.Write(".tuple"); + } + output.Write(' '); + output.Write('('); + Variable.WriteTo(output); + output.Write(" = "); + TestedOperand.WriteTo(output, options); + output.Write(')'); + if (SubPatterns.Count > 0) + { + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + foreach (var pattern in SubPatterns) + { + pattern.WriteTo(output, options); + output.WriteLine(); + } + output.Unindent(); + output.Write('}'); + output.MarkFoldEnd(); + } + } + finally + { + output.MarkNodeEnd(this); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs b/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs index a8e333344d..75c7b191a7 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs @@ -39,27 +39,35 @@ partial class LdObj { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - if (options.UseFieldSugar) + output.MarkNodeStart(this); + try { - if (this.MatchLdFld(out var target, out var field)) + if (options.UseFieldSugar) { - WriteILRange(output, options); - output.Write("ldfld "); - field.WriteTo(output); - output.Write('('); - target.WriteTo(output, options); - output.Write(')'); - return; - } - else if (this.MatchLdsFld(out field)) - { - WriteILRange(output, options); - output.Write("ldsfld "); - field.WriteTo(output); - return; + if (this.MatchLdFld(out var target, out var field)) + { + WriteILRange(output, options); + output.Write("ldfld "); + field.WriteTo(output); + output.Write('('); + target.WriteTo(output, options); + output.Write(')'); + return; + } + else if (this.MatchLdsFld(out field)) + { + WriteILRange(output, options); + output.Write("ldsfld "); + field.WriteTo(output); + return; + } } + OriginalWriteTo(output, options); + } + finally + { + output.MarkNodeEnd(this); } - OriginalWriteTo(output, options); } } @@ -67,32 +75,40 @@ partial class StObj { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - if (options.UseFieldSugar) + output.MarkNodeStart(this); + try { - if (this.MatchStFld(out var target, out var field, out var value)) + if (options.UseFieldSugar) { - WriteILRange(output, options); - output.Write("stfld "); - field.WriteTo(output); - output.Write('('); - target.WriteTo(output, options); - output.Write(", "); - value.WriteTo(output, options); - output.Write(')'); - return; - } - else if (this.MatchStsFld(out field, out value)) - { - WriteILRange(output, options); - output.Write("stsfld "); - field.WriteTo(output); - output.Write('('); - value.WriteTo(output, options); - output.Write(')'); - return; + if (this.MatchStFld(out var target, out var field, out var value)) + { + WriteILRange(output, options); + output.Write("stfld "); + field.WriteTo(output); + output.Write('('); + target.WriteTo(output, options); + output.Write(", "); + value.WriteTo(output, options); + output.Write(')'); + return; + } + else if (this.MatchStsFld(out field, out value)) + { + WriteILRange(output, options); + output.Write("stsfld "); + field.WriteTo(output); + output.Write('('); + value.WriteTo(output, options); + output.Write(')'); + return; + } } + OriginalWriteTo(output, options); + } + finally + { + output.MarkNodeEnd(this); } - OriginalWriteTo(output, options); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs index 0d6ad6cbed..b8c8f7d3e7 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs @@ -92,13 +92,21 @@ protected override InstructionFlags ComputeFlags() public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write("("); - valueInst.WriteTo(output, options); - output.Write(", "); - fallbackInst.WriteTo(output, options); - output.Write(")"); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write("("); + valueInst.WriteTo(output, options); + output.Write(", "); + fallbackInst.WriteTo(output, options); + output.Write(")"); + } + finally + { + output.MarkNodeEnd(this); + } } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs b/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs index b1726c43a7..ab60c94931 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs @@ -92,15 +92,23 @@ internal override void CheckInvariant(ILPhase phase) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - output.Write("nullable.unwrap."); - if (RefInput) + output.MarkNodeStart(this); + try { - output.Write("refinput."); + output.Write("nullable.unwrap."); + if (RefInput) + { + output.Write("refinput."); + } + output.Write(ResultType); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); + } + finally + { + output.MarkNodeEnd(this); } - output.Write(ResultType); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); } public override StackType ResultType { get; } diff --git a/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs index d5d83ab1ac..3b58211997 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs @@ -26,9 +26,17 @@ public abstract partial class SimpleInstruction : ILInstruction { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - // the non-custom WriteTo would add useless parentheses + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + // the non-custom WriteTo would add useless parentheses + } + finally + { + output.MarkNodeEnd(this); + } } } @@ -46,15 +54,23 @@ partial class Nop public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (Kind != NopKind.Normal) + output.MarkNodeStart(this); + try { - output.Write("." + Kind.ToString().ToLowerInvariant()); + WriteILRange(output, options); + output.Write(OpCode); + if (Kind != NopKind.Normal) + { + output.Write("." + Kind.ToString().ToLowerInvariant()); + } + if (!string.IsNullOrEmpty(Comment)) + { + output.Write(" // " + Comment); + } } - if (!string.IsNullOrEmpty(Comment)) + finally { - output.Write(" // " + Comment); + output.MarkNodeEnd(this); } } } @@ -75,13 +91,21 @@ public override StackType ResultType { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (!string.IsNullOrEmpty(Message)) + output.MarkNodeStart(this); + try { - output.Write("(\""); - output.Write(Message); - output.Write("\")"); + WriteILRange(output, options); + output.Write(OpCode); + if (!string.IsNullOrEmpty(Message)) + { + output.Write("(\""); + output.Write(Message); + output.Write("\")"); + } + } + finally + { + output.MarkNodeEnd(this); } } } @@ -103,13 +127,21 @@ public override StackType ResultType { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (!string.IsNullOrEmpty(Message)) + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(OpCode); + if (!string.IsNullOrEmpty(Message)) + { + output.Write("(\""); + output.Write(Message); + output.Write("\")"); + } + } + finally { - output.Write("(\""); - output.Write(Message); - output.Write("\")"); + output.MarkNodeEnd(this); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs b/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs index cdb9bee8a6..8802b8d647 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs @@ -54,24 +54,32 @@ public StringToInt(ILInstruction argument, string?[] map, IType expectedType) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write("string.to.int "); - ExpectedType.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(", { "); - int i = 0; - foreach (var entry in Map) + output.MarkNodeStart(this); + try { - if (i > 0) - output.Write(", "); - if (entry.Key is null) - output.Write($"[null] = {entry.Value}"); - else - output.Write($"[\"{entry.Key}\"] = {entry.Value}"); - i++; + WriteILRange(output, options); + output.Write("string.to.int "); + ExpectedType.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(", { "); + int i = 0; + foreach (var entry in Map) + { + if (i > 0) + output.Write(", "); + if (entry.Key is null) + output.Write($"[null] = {entry.Value}"); + else + output.Write($"[\"{entry.Key}\"] = {entry.Value}"); + i++; + } + output.Write(" })"); + } + finally + { + output.MarkNodeEnd(this); } - output.Write(" })"); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs index 8800626202..fdfb2bed66 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs @@ -85,26 +85,34 @@ public override InstructionFlags DirectFlags { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write("switch"); - if (IsLifted) - output.Write(".lifted"); - output.Write(' '); - Type?.WriteTo(output); - output.Write('('); - value.WriteTo(output, options); - output.Write(") "); - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - foreach (var section in this.Sections) + output.MarkNodeStart(this); + try { - section.WriteTo(output, options); - output.WriteLine(); + WriteILRange(output, options); + output.Write("switch"); + if (IsLifted) + output.Write(".lifted"); + output.Write(' '); + Type?.WriteTo(output); + output.Write('('); + value.WriteTo(output, options); + output.Write(") "); + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + foreach (var section in this.Sections) + { + section.WriteTo(output, options); + output.WriteLine(); + } + output.Unindent(); + output.Write('}'); + output.MarkFoldEnd(); + } + finally + { + output.MarkNodeEnd(this); } - output.Unindent(); - output.Write('}'); - output.MarkFoldEnd(); } protected override int GetChildCount() @@ -226,27 +234,35 @@ public override InstructionFlags DirectFlags { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - if (IsCompilerGeneratedDefaultSection) - output.Write("generated."); - output.WriteLocalReference("case", this, isDefinition: true); - output.Write(' '); - if (HasNullLabel) + output.MarkNodeStart(this); + try { - output.Write("null"); - if (!Labels.IsEmpty) + WriteILRange(output, options); + if (IsCompilerGeneratedDefaultSection) + output.Write("generated."); + output.WriteLocalReference("case", this, isDefinition: true); + output.Write(' '); + if (HasNullLabel) + { + output.Write("null"); + if (!Labels.IsEmpty) + { + output.Write(", "); + output.Write(Labels.ToString()); + } + } + else { - output.Write(", "); output.Write(Labels.ToString()); } + output.Write(": "); + + body.WriteTo(output, options); } - else + finally { - output.Write(Labels.ToString()); + output.MarkNodeEnd(this); } - output.Write(": "); - - body.WriteTo(output, options); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs index f8a7da9935..f4d45c9d5e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs @@ -70,13 +70,21 @@ public override ILInstruction Clone() public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(".try "); - TryBlock.WriteTo(output, options); - foreach (var handler in Handlers) + output.MarkNodeStart(this); + try { - output.Write(' '); - handler.WriteTo(output, options); + WriteILRange(output, options); + output.Write(".try "); + TryBlock.WriteTo(output, options); + foreach (var handler in Handlers) + { + output.Write(' '); + handler.WriteTo(output, options); + } + } + finally + { + output.MarkNodeEnd(this); } } @@ -166,19 +174,27 @@ public override InstructionFlags DirectFlags { public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write("catch "); - if (variable != null) + output.MarkNodeStart(this); + try { - output.WriteLocalReference(variable.Name, variable, isDefinition: true); - output.Write(" : "); - Disassembler.DisassemblerHelpers.WriteOperand(output, variable.Type); + WriteILRange(output, options); + output.Write("catch "); + if (variable != null) + { + output.WriteLocalReference(variable.Name, variable, isDefinition: true); + output.Write(" : "); + Disassembler.DisassemblerHelpers.WriteOperand(output, variable.Type); + } + output.Write(" when ("); + filter.WriteTo(output, options); + output.Write(')'); + output.Write(' '); + body.WriteTo(output, options); + } + finally + { + output.MarkNodeEnd(this); } - output.Write(" when ("); - filter.WriteTo(output, options); - output.Write(')'); - output.Write(' '); - body.WriteTo(output, options); } /// @@ -219,11 +235,19 @@ public override ILInstruction Clone() public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(".try "); - TryBlock.WriteTo(output, options); - output.Write(" finally "); - finallyBlock.WriteTo(output, options); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(".try "); + TryBlock.WriteTo(output, options); + output.Write(" finally "); + finallyBlock.WriteTo(output, options); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType { @@ -316,11 +340,19 @@ public override ILInstruction Clone() public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(".try "); - TryBlock.WriteTo(output, options); - output.Write(" fault "); - faultBlock.WriteTo(output, options); + output.MarkNodeStart(this); + try + { + WriteILRange(output, options); + output.Write(".try "); + TryBlock.WriteTo(output, options); + output.Write(" fault "); + faultBlock.WriteTo(output, options); + } + finally + { + output.MarkNodeEnd(this); + } } public override StackType ResultType { diff --git a/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs index 2ae492af85..9cb0ef47d9 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs @@ -52,15 +52,23 @@ internal override void CheckInvariant(ILPhase phase) public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write(OpCode); - if (IsLifted) + output.MarkNodeStart(this); + try { - output.Write(".lifted"); + WriteILRange(output, options); + output.Write(OpCode); + if (IsLifted) + { + output.Write(".lifted"); + } + output.Write('('); + this.Argument.WriteTo(output, options); + output.Write(')'); + } + finally + { + output.MarkNodeEnd(this); } - output.Write('('); - this.Argument.WriteTo(output, options); - output.Write(')'); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs index f082457ffc..d0600df327 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs @@ -42,26 +42,34 @@ partial class UsingInstruction public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { - WriteILRange(output, options); - output.Write("using"); - if (IsAsync) + output.MarkNodeStart(this); + try { - output.Write(".async"); + WriteILRange(output, options); + output.Write("using"); + if (IsAsync) + { + output.Write(".async"); + } + if (IsRefStruct) + { + output.Write(".ref"); + } + output.Write(" ("); + Variable.WriteTo(output); + output.Write(" = "); + ResourceExpression.WriteTo(output, options); + output.WriteLine(") {"); + output.Indent(); + Body.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write("}"); } - if (IsRefStruct) + finally { - output.Write(".ref"); + output.MarkNodeEnd(this); } - output.Write(" ("); - Variable.WriteTo(output); - output.Write(" = "); - ResourceExpression.WriteTo(output, options); - output.WriteLine(") {"); - output.Indent(); - Body.WriteTo(output, options); - output.Unindent(); - output.WriteLine(); - output.Write("}"); } } } 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..9436e30518 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs @@ -120,5 +120,24 @@ 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; + if (!step.ModifiedNodeCandidates.Contains(modifiedNode)) + step.ModifiedNodeCandidates.Insert(0, modifiedNode); + } + } } } 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 index 7976749bce..ad396f932c 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs @@ -114,6 +114,15 @@ private Node StepInternal(string description, ILInstruction? near, object? modif BeginStep = step, EndStep = step + 1 }; + // Record the IL position and its ancestor chain as highlight candidates here, 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. A later transform may detach the exact + // instruction, but a surviving ancestor (ultimately the ILFunction) still resolves. + for (var node = near; node != null; node = node.Parent) + { + stepNode.ModifiedNodeCandidates.Add(node); + } if (step == StepLimit) { LimitReachedStep = stepNode; 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 f836062df1..4c26dadb0e 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -193,6 +193,72 @@ static string StripStepNumber(string 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!; + + tab.RestartDecompileWithStepLimit(replayStep!.BeginStep, isDebug: false, replayStep.BeginStep); + tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + tab.Text.Should().NotBeNullOrWhiteSpace("ILAst replay before a selected step must still emit IL"); + tab.DebugStepHighlight.Should().NotBeNull("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"); + + tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep); + tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + tab.Text.Should().NotBeNullOrWhiteSpace("ILAst replay after a selected step must still emit IL"); + tab.DebugStepHighlight.Should().NotBeNull("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 ICSharpCode.Decompiler.IL.Transforms.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() { diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index cf6ca92f56..4439e40ee1 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -343,11 +343,11 @@ 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, decompiler.Decompile(members), decompiler); + WriteCode(output, options, decompiler.Decompile(members), decompiler.Stepper); } else { - WriteCode(output, options, decompiler.Decompile(method.MetadataToken), decompiler); + WriteCode(output, options, decompiler.Decompile(method.MetadataToken), decompiler.Stepper); } OnCSharpDecompiled(decompiler, output, options); } @@ -360,7 +360,7 @@ public override void DecompileProperty(IProperty property, ITextOutput output, D { CSharpDecompiler decompiler = BeginDecompile(property, output, options); WriteCommentLine(output, TypeToString(property.DeclaringType)); - WriteCode(output, options, decompiler.Decompile(property.MetadataToken), decompiler); + WriteCode(output, options, decompiler.Decompile(property.MetadataToken), decompiler.Stepper); OnCSharpDecompiled(decompiler, output, options); } @@ -370,14 +370,14 @@ public override void DecompileField(IField field, ITextOutput output, Decompilat WriteCommentLine(output, TypeToString(field.DeclaringType)); if (field.IsConst) { - WriteCode(output, options, decompiler.Decompile(field.MetadataToken), decompiler); + 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, decompiler.Decompile(members), decompiler); + WriteCode(output, options, decompiler.Decompile(members), decompiler.Stepper); } OnCSharpDecompiled(decompiler, output, options); } @@ -406,7 +406,7 @@ 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, decompiler.DecompileExtension(extension.MetadataToken), decompiler); + WriteCode(output, options, decompiler.DecompileExtension(extension.MetadataToken), decompiler.Stepper); OnCSharpDecompiled(decompiler, output, options); } @@ -414,7 +414,7 @@ public override void DecompileEvent(IEvent ev, ITextOutput output, Decompilation { CSharpDecompiler decompiler = BeginDecompile(ev, output, options); WriteCommentLine(output, TypeToString(ev.DeclaringType)); - WriteCode(output, options, decompiler.Decompile(ev.MetadataToken), decompiler); + WriteCode(output, options, decompiler.Decompile(ev.MetadataToken), decompiler.Stepper); OnCSharpDecompiled(decompiler, output, options); } @@ -422,7 +422,7 @@ public override void DecompileType(ITypeDefinition type, ITextOutput output, Dec { CSharpDecompiler decompiler = BeginDecompile(type, output, options); WriteCommentLine(output, TypeToString(type, ConversionFlags.UseFullyQualifiedTypeNames | ConversionFlags.UseFullyQualifiedEntityNames)); - WriteCode(output, options, decompiler.Decompile(type.MetadataToken), decompiler); + WriteCode(output, options, decompiler.Decompile(type.MetadataToken), decompiler.Stepper); OnCSharpDecompiled(decompiler, output, options); } @@ -509,7 +509,7 @@ public override void DecompileType(ITypeDefinition type, ITextOutput output, Dec SyntaxTree st = options.FullDecompilation ? decompiler.DecompileWholeModuleAsSingleFile() : decompiler.DecompileModuleAndAssemblyAttributes(); - WriteCode(output, options, st, decompiler); + WriteCode(output, options, st, decompiler.Stepper); return null; } @@ -707,12 +707,12 @@ public void Run(AstNode rootNode, TransformContext context) } } - static void WriteCode(ITextOutput output, DecompilationOptions options, SyntaxTree syntaxTree, CSharpDecompiler decompiler) + 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); + TokenWriter tokenWriter = new TextTokenWriter(output, settings); if (output is TextView.AvaloniaEditTextOutput avaloniaOutput) tokenWriter = new CSharpHighlightingTokenWriter(tokenWriter, avaloniaOutput); else if (output is TextView.ISmartTextOutput smartOutput) @@ -729,48 +729,12 @@ static void WriteCode(ITextOutput output, DecompilationOptions options, SyntaxTr syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); bookmarkCollector?.Publish(); if (output is TextView.AvaloniaEditTextOutput nodeOutput - && TryGetDebugStepHighlightRange(decompiler, options, nodeOutput.NodeLookup, out var range)) + && TextView.DebugStepHighlighter.TryResolve(stepper, options.StepLimit, options.HighlightStep, nodeOutput.NodeLookup, out var range)) { nodeOutput.DebugStepHighlight = range; } } - static bool TryGetDebugStepHighlightRange(CSharpDecompiler decompiler, DecompilationOptions options, TextView.NodeLookup nodeLookup, out TextView.TextRange range) - { - range = default; - if (options.StepLimit == int.MaxValue) - return false; - if (options.HighlightStep is { } highlightStep) - { - if (TryGetRange(decompiler.Stepper.GetStepByBeginStep(highlightStep), nodeLookup, out range)) - return true; - if (decompiler.Stepper.LimitReachedStep is { BeginStep: var limitStep } reachedStep - && limitStep == highlightStep) - { - return TryGetRange(reachedStep, nodeLookup, out range); - } - return false; - } - if (TryGetRange(decompiler.Stepper.LimitReachedStep, nodeLookup, out range)) - return true; - if (options.StepLimit > 0) - return TryGetRange(decompiler.Stepper.GetStepByBeginStep(options.StepLimit - 1), nodeLookup, out range); - return false; - - static bool TryGetRange(Stepper.Node? step, TextView.NodeLookup nodeLookup, out TextView.TextRange range) - { - range = default; - if (step == null) - return false; - foreach (var candidate in step.ModifiedNodeCandidates) - { - if (nodeLookup.TryGetRange(candidate, out range)) - return true; - } - return step.ModifiedNode != null && nodeLookup.TryGetRange(step.ModifiedNode, out range); - } - } - void AddWarningMessage(MetadataFile module, ITextOutput output, string line1, string? line2 = null, string? buttonText = null, global::Avalonia.Media.IImage? buttonImage = null, System.EventHandler? buttonClickHandler = null) diff --git a/ILSpy/Languages/ILAstLanguage.cs b/ILSpy/Languages/ILAstLanguage.cs index ff11e669ce..eaa6b6f9c8 100644 --- a/ILSpy/Languages/ILAstLanguage.cs +++ b/ILSpy/Languages/ILAstLanguage.cs @@ -188,6 +188,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 6fa7f17478..3ad943b1c6 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(); @@ -306,12 +306,12 @@ public void EndSpan() } } - internal void MarkNodeStart(object node) + public void MarkNodeStart(object node) { openNodes.Push((node, builder.Length)); } - internal void MarkNodeEnd(object node) + public void MarkNodeEnd(object node) { if (openNodes.Count == 0) return; diff --git a/ILSpy/TextView/DebugStepHighlighter.cs b/ILSpy/TextView/DebugStepHighlighter.cs new file mode 100644 index 0000000000..ceba4a1bdc --- /dev/null +++ b/ILSpy/TextView/DebugStepHighlighter.cs @@ -0,0 +1,73 @@ +// 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.IL.Transforms; + +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; + foreach (var candidate in step.ModifiedNodeCandidates) + { + if (nodeLookup.TryGetRange(candidate, out range)) + return true; + } + return step.ModifiedNode != null && nodeLookup.TryGetRange(step.ModifiedNode, out range); + } + } +} From dddaada4e60279ac611101c0a3f08ae562df625b Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 28 Jun 2026 08:45:34 +0200 Subject: [PATCH 04/21] Await debug-step replay decompiles to fix flaky UI tests The Debug Steps "show state before/after" replay re-decompiles the active tab; the headless UI tests inferred completion by polling IsDecompiling + Text, which can return on stale state or hit the 60s wait deadline when that shared signal races under CI load (the observed intermittent CI timeout). RestartDecompileWithStepLimit now returns the decompile Task so the replay completion can be awaited deterministically; the tests await it instead of polling. The production IsDecompiling reset is unchanged -- the last decompile's finally always resets it; the race was only in the test's completion inference. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.Tests/Views/DebugStepsTests.cs | 12 ++++-------- ILSpy/TextView/DecompilerTabPageModel.cs | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 4c26dadb0e..1413e30ac4 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -172,15 +172,13 @@ await Waiters.WaitForAsync( var replayStep = transformGroupWithChanges.Children.First(); var tab = vm.DockWorkspace.ActiveDecompilerTab!; - tab.RestartDecompileWithStepLimit(replayStep.BeginStep, isDebug: false, replayStep.BeginStep); - tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + 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"); tab.DebugStepHighlight.Should().NotBeNull("C# replay before a selected AST mutation step must locate the changed node"); debugStepsVm.Steps.Should().BeSameAs(collectedSteps, "a step-limited C# replay must not replace the full step tree shown by the pane"); - tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep); - tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + 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"); tab.DebugStepHighlight.Should().NotBeNull("C# replay after a selected AST mutation step must locate the changed node"); debugStepsVm.Steps.Should().BeSameAs(collectedSteps, @@ -226,15 +224,13 @@ await Waiters.WaitForAsync( var collectedSteps = debugStepsVm.Steps; var tab = vm.DockWorkspace.ActiveDecompilerTab!; - tab.RestartDecompileWithStepLimit(replayStep!.BeginStep, isDebug: false, replayStep.BeginStep); - tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + await tab.RestartDecompileWithStepLimit(replayStep!.BeginStep, isDebug: false, replayStep.BeginStep); tab.Text.Should().NotBeNullOrWhiteSpace("ILAst replay before a selected step must still emit IL"); tab.DebugStepHighlight.Should().NotBeNull("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"); - tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep); - tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + await tab.RestartDecompileWithStepLimit(replayStep.EndStep, isDebug: false, replayStep.BeginStep); tab.Text.Should().NotBeNullOrWhiteSpace("ILAst replay after a selected step must still emit IL"); tab.DebugStepHighlight.Should().NotBeNull("ILAst replay after a selected step must locate the changed instruction"); diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index 4091e30128..34376996e0 100644 --- a/ILSpy/TextView/DecompilerTabPageModel.cs +++ b/ILSpy/TextView/DecompilerTabPageModel.cs @@ -401,12 +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, int? highlightStep = null) + 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 @@ -463,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; } /// From e173a82ebd43eb209cc3f53ebed7b718a1c827a1 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 28 Jun 2026 21:58:50 +0200 Subject: [PATCH 05/21] Exclude leading indentation from highlight position ranges MarkNodeStart and BeginSpan captured builder.Length to anchor a node range or highlight span, but indentation is written lazily on the first token of a line. A node or span opened at the start of an indented line therefore recorded its start before the leading tabs, so the debug-step highlight (and any span) extended back across the indentation to column 0. Flush the pending indent in both before capturing the offset, matching the WPF AvalonEditTextOutput.BeginSpan the Avalonia port derived from. The emitted text is unchanged -- the indent is written either way, in the same place; only the recorded start moves to the first real character. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.Tests/Views/DebugStepsTests.cs | 43 ++++++++++++++++++++++++ ILSpy/TextView/AvaloniaEditTextOutput.cs | 20 ++++++----- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 1413e30ac4..39014f5155 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -288,6 +288,49 @@ public Task NodeLookup_Resolves_Copied_Ast_Annotations() 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(); + 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(); + 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() { diff --git a/ILSpy/TextView/AvaloniaEditTextOutput.cs b/ILSpy/TextView/AvaloniaEditTextOutput.cs index 3ad943b1c6..7c3c25f2a6 100644 --- a/ILSpy/TextView/AvaloniaEditTextOutput.cs +++ b/ILSpy/TextView/AvaloniaEditTextOutput.cs @@ -54,7 +54,10 @@ public sealed class AvaloniaEditTextOutput : ISmartTextOutput, INodeTrackingOutp public int LengthLimit { get; set; } = int.MaxValue; readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new(); - readonly Stack<(object Node, int Offset)> openNodes = 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; @@ -290,6 +293,7 @@ public void AddVisualLineElementGenerator(VisualLineElementGenerator generator) public void BeginSpan(HighlightingColor highlightingColor) { + WriteIndentIfNeeded(); openSpans.Push((builder.Length, highlightingColor)); } @@ -308,17 +312,17 @@ public void EndSpan() public void MarkNodeStart(object node) { - openNodes.Push((node, builder.Length)); + // 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 (openNodes.Count == 0) - return; - var (currentNode, start) = openNodes.Pop(); - if (!ReferenceEquals(currentNode, node)) - return; - NodeLookup.AddNode(node, start, builder.Length - start); + if (openNodeStarts.Remove(node, out var start)) + NodeLookup.AddNode(node, start, builder.Length - start); } } } From 1d7f6f3d147affcf3c2c3b3a0154aab324a1bca5 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 09:21:57 +0200 Subject: [PATCH 06/21] Center debug-step highlight via fold-aware bookmark helper The debug-step highlight centred with a logical (line-number * line-height) calculation posted at default dispatcher priority. That ignores collapsed foldings above the target, races the layout of a just-applied document (silently no-oping when the viewport is not yet measured), and does not recheck the document, so a newer decompile could scroll the wrong content. The symptom was unreliable centring, most visible in the ILAst view whose nested block output is fold-dense and larger. Route the highlight through the existing CenterLineInView helper that bookmark navigation already uses: it centres via GetVisualTopByDocumentLine (fold-aware), runs at Background priority so the document finishes measuring first, and rechecks the document identity. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy/TextView/DecompilerTextView.axaml.cs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index 4a0e559145..a12965d04d 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -1265,23 +1265,17 @@ void ApplyDebugStepHighlight(TextRange? range) debugStepMarks.Add(mark); Editor.TextArea.Caret.Offset = start; Editor.TextArea.Caret.BringCaretToView(); - Dispatcher.UIThread.Post(() => CenterDocumentOffsetInView(start)); + // 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; + Dispatcher.UIThread.Post(() => CenterLineInView(document, line), DispatcherPriority.Background); CaretHighlightAdorner.DisplayCaretHighlightAnimation(Editor.TextArea); } - void CenterDocumentOffsetInView(int offset) - { - if (EditorScrollViewer is not { } scrollViewer || Editor.Document.TextLength == 0) - return; - offset = Math.Clamp(offset, 0, Editor.Document.TextLength); - var line = Editor.Document.GetLineByOffset(offset); - var lineHeight = Editor.TextArea.TextView.DefaultLineHeight; - if (lineHeight <= 0 || scrollViewer.Viewport.Height <= 0) - return; - var targetY = Math.Max(0, (line.LineNumber - 1) * lineHeight - (scrollViewer.Viewport.Height - lineHeight) / 2); - scrollViewer.Offset = new Vector(scrollViewer.Offset.X, targetY); - } - void ClearDebugStepMarks() { foreach (var mark in debugStepMarks) From a85e520d303340bfb97597467402cf4bae2cdbec Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 09:51:50 +0200 Subject: [PATCH 07/21] Skip debug-step node tracking on normal decompiles NodeLookup population runs per AST node (and per annotation) on every on-screen C# decompile, but the debug-step highlighter only consumes it when a step limit is set. Gate the node-tracking token writer on StepLimit so the common Release path skips the bookkeeping; AvaloniaEditTextOutput is an ISmartTextOutput, so it still gets full syntax highlighting via the existing branch. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy/Languages/CSharpLanguage.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 4439e40ee1..679f2317da 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -713,7 +713,11 @@ static void WriteCode(ITextOutput output, DecompilationOptions options, SyntaxTr syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); output.IndentationString = settings.CSharpFormattingOptions.IndentationString; TokenWriter tokenWriter = new TextTokenWriter(output, settings); - if (output is TextView.AvaloniaEditTextOutput avaloniaOutput) + // 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); From f176f502eeff2a183f1b20f0c479e54e2c3edf5d Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 11:20:28 +0200 Subject: [PATCH 08/21] Point a caret at the gap for debug steps that remove a node A step that removes a node has nothing left to highlight in the resulting text, so range resolution fell back to the enclosing block and flooded it. Record the changed node's surviving neighbours as seam anchors (captured before the mutation) and split a step's candidates into precise / seam / ancestor tiers: when neither the node nor its marker resolves, place a zero-length caret at the gap -- the successor's start, else the predecessor's end -- and only fall back to the enclosing block when no neighbour survives. A zero-length highlight is rendered as a caret (positioned, pulsed, centered) with no background mark. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../CSharp/Transforms/TransformContext.cs | 13 +++++- .../IL/Transforms/Stepper.cs | 42 +++++++++++++++--- ILSpy.Tests/Views/DebugStepsTests.cs | 44 +++++++++++++++++++ ILSpy/TextView/DebugStepHighlighter.cs | 22 +++++++++- ILSpy/TextView/DecompilerTextView.axaml.cs | 20 ++++++--- 5 files changed, 124 insertions(+), 17 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs index bb8d8cb965..9adf66b96e 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs @@ -120,9 +120,18 @@ static void TrackModifiedNode(Stepper.Node step, AstNode? modifiedNode, bool ins var marker = new DebugStepMarker(); modifiedNode.AddAnnotation(marker); AddCandidate(step, marker, insertFirst: false); - for (var parent = modifiedNode.Parent; parent != null; parent = parent.Parent) + // insertFirst marks the produced-node update from EndStep; the seam neighbours and + // ancestor chain are recorded once, from the original node captured before the mutation. + if (!insertFirst) { - AddCandidate(step, parent, insertFirst: false); + if (modifiedNode.NextSibling is { } nextSibling) + step.SeamAnchors.Add((nextSibling, false)); + if (modifiedNode.PrevSibling is { } prevSibling) + step.SeamAnchors.Add((prevSibling, true)); + for (var parent = modifiedNode.Parent; parent != null; parent = parent.Parent) + { + step.AncestorCandidates.Add(parent); + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs index ad396f932c..cc45a46900 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs @@ -65,8 +65,23 @@ public class Node public string Description { get; } public ILInstruction? 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(); /// + /// Neighbours 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 neighbour, 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 neighbour has a rendered range. + /// + public IList AncestorCandidates { get; } = new List(); + /// /// BeginStep is inclusive. /// public int BeginStep { get; set; } @@ -114,14 +129,27 @@ private Node StepInternal(string description, ILInstruction? near, object? modif BeginStep = step, EndStep = step + 1 }; - // Record the IL position and its ancestor chain as highlight candidates here, 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. A later transform may detach the exact - // instruction, but a surviving ancestor (ultimately the ILFunction) still resolves. - for (var node = near; node != null; node = node.Parent) + // Record the IL position, its neighbours, and its ancestor chain as highlight candidates + // here, 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. A later transform may detach the + // exact instruction; a surviving neighbour then anchors a seam caret, and failing that a + // surviving ancestor (ultimately the ILFunction) still resolves. + if (near != null) { - stepNode.ModifiedNodeCandidates.Add(node); + stepNode.ModifiedNodeCandidates.Add(near); + if (near.Parent is { } parent) + { + int index = near.ChildIndex; + if (index + 1 < parent.Children.Count) + stepNode.SeamAnchors.Add((parent.Children[index + 1], false)); + if (index - 1 >= 0) + stepNode.SeamAnchors.Add((parent.Children[index - 1], true)); + } + for (var node = near.Parent; node != null; node = node.Parent) + { + stepNode.AncestorCandidates.Add(node); + } } if (step == StepLimit) { diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 39014f5155..792742a9e3 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -288,6 +288,50 @@ public Task NodeLookup_Resolves_Copied_Ast_Annotations() 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 neighbours. 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 ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("0: Remove statement") { + BeginStep = 0, + EndStep = 1 + }; + removalWithSuccessor.SeamAnchors.Add((successor, false)); + removalWithSuccessor.SeamAnchors.Add((predecessor, true)); + var successorStepper = new ICSharpCode.Decompiler.IL.Transforms.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 neighbour"); + 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 ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("0: Remove statement") { + BeginStep = 0, + EndStep = 1 + }; + removalWithPredecessor.SeamAnchors.Add((successor, false)); + removalWithPredecessor.SeamAnchors.Add((predecessor, true)); + var predecessorStepper = new ICSharpCode.Decompiler.IL.Transforms.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 MarkNodeStart_Excludes_Leading_Indentation() { diff --git a/ILSpy/TextView/DebugStepHighlighter.cs b/ILSpy/TextView/DebugStepHighlighter.cs index ceba4a1bdc..93d83f6105 100644 --- a/ILSpy/TextView/DebugStepHighlighter.cs +++ b/ILSpy/TextView/DebugStepHighlighter.cs @@ -62,12 +62,32 @@ static bool TryGetRange(Stepper.Node? step, NodeLookup nodeLookup, out TextRange 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; } - return step.ModifiedNode != null && nodeLookup.TryGetRange(step.ModifiedNode, out range); + 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 neighbour, 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/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index a12965d04d..edbb11c0f4 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -1254,15 +1254,21 @@ void ClearLocalReferenceMarks() void ApplyDebugStepHighlight(TextRange? range) { ClearDebugStepMarks(); - if (range is not { } r || r.Length <= 0) + if (range is not { } r || r.Length < 0) return; var start = Math.Clamp(r.Start, 0, Editor.Document.TextLength); - var end = Math.Clamp(r.Start + r.Length, start, Editor.Document.TextLength); - if (end <= start) - return; - var mark = textMarkerService.Create(start, end - start); - mark.BackgroundColor = DebugStepBackground; - debugStepMarks.Add(mark); + // 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 From 7469de415e2b2d0a82e32dc7a0e119c37494f819 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 11:38:08 +0200 Subject: [PATCH 09/21] Add a filter box to the Debug Steps pane The step tree can run to hundreds of entries per decompile, so finding a specific mutation means scrolling. Add a filter box in the pane's top-right corner: a row survives when its description -- or any descendant's -- contains the text (case-insensitive), keeping the path to every match, and the tree auto-expands while filtering so matches nested under transform groups stay visible. Implemented as an item-visibility converter over the existing TreeView rather than switching to SharpTreeView, which would change the Steps contract and rewrite the pane's tests for no functional gain here. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.Tests/Views/DebugStepsTests.cs | 49 ++++++++++++++++++++ ILSpy/ViewModels/DebugStepsPaneModel.cs | 14 ++++++ ILSpy/Views/DebugStepFilterConverter.cs | 60 +++++++++++++++++++++++++ ILSpy/Views/DebugSteps.axaml | 60 +++++++++++++++++-------- 4 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 ILSpy/Views/DebugStepFilterConverter.cs diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 792742a9e3..1690169e3b 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -19,11 +19,13 @@ #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; @@ -332,6 +334,53 @@ public Task DebugStepHighlighter_Removal_Resolves_To_Seam_Caret() return Task.CompletedTask; } + [AvaloniaTest] + public Task DebugStepFilter_Keeps_Matches_And_The_Path_To_Them() + { + var converter = new DebugStepFilterConverter(); + var matchingLeaf = new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("3: Introduce query continuation"); + var otherLeaf = new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("4: Flatten switch section block"); + var group = new ICSharpCode.Decompiler.IL.Transforms.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(ICSharpCode.Decompiler.IL.Transforms.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 ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("CombineQueryExpressions"); + group.Children.Add(new ICSharpCode.Decompiler.IL.Transforms.Stepper.Node("3: Introduce query continuation")); + group.Children.Add(new ICSharpCode.Decompiler.IL.Transforms.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; + } + [AvaloniaTest] public Task MarkNodeStart_Excludes_Leading_Indentation() { diff --git a/ILSpy/ViewModels/DebugStepsPaneModel.cs b/ILSpy/ViewModels/DebugStepsPaneModel.cs index b0dc7ff701..e264606085 100644 --- a/ILSpy/ViewModels/DebugStepsPaneModel.cs +++ b/ILSpy/ViewModels/DebugStepsPaneModel.cs @@ -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; } diff --git a/ILSpy/Views/DebugStepFilterConverter.cs b/ILSpy/Views/DebugStepFilterConverter.cs new file mode 100644 index 0000000000..f94417afba --- /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.IL.Transforms; + +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..5946530330 100644 --- a/ILSpy/Views/DebugSteps.axaml +++ b/ILSpy/Views/DebugSteps.axaml @@ -3,40 +3,64 @@ 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"> + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + From b7286854ef2e05373abf7ead4378706510c2dce1 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 12:05:30 +0200 Subject: [PATCH 10/21] Bracket ILAst WriteTo in one place via WriteToCore The debug-step node bracket (MarkNodeStart/try/finally/MarkNodeEnd) was copied into every hand-written WriteTo override and re-emitted by the T4 generator, so a newly added instruction could silently omit it and lose step highlighting with no compile error or test failure. Seal WriteTo on ILInstruction to apply the bracket once and delegate to a new abstract WriteToCore; move every override (hand-written and generated) to WriteToCore without the wrapper. Rendered output is unchanged -- the marks are no-ops unless the output tracks nodes. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ICSharpCode.Decompiler/IL/Instructions.cs | 887 +++++++----------- ICSharpCode.Decompiler/IL/Instructions.tt | 8 +- .../Instructions/BinaryNumericInstruction.cs | 54 +- .../IL/Instructions/Block.cs | 60 +- .../IL/Instructions/BlockContainer.cs | 80 +- .../IL/Instructions/Branch.cs | 18 +- .../IL/Instructions/CallIndirect.cs | 42 +- .../IL/Instructions/CallInstruction.cs | 42 +- .../IL/Instructions/Comp.cs | 72 +- .../CompoundAssignmentInstruction.cs | 110 +-- .../IL/Instructions/Conv.cs | 78 +- .../IL/Instructions/DeconstructInstruction.cs | 58 +- .../DeconstructResultInstruction.cs | 24 +- .../IL/Instructions/DynamicInstructions.cs | 288 ++---- .../IL/Instructions/ExpressionTreeCast.cs | 28 +- .../IL/Instructions/ILFunction.cs | 140 ++- .../IL/Instructions/ILInstruction.cs | 24 +- .../IL/Instructions/IfInstruction.cs | 58 +- .../IL/Instructions/LdLen.cs | 24 +- .../IL/Instructions/Leave.cs | 26 +- .../IL/Instructions/LockInstruction.cs | 28 +- .../IL/Instructions/MatchInstruction.cs | 92 +- .../IL/Instructions/MemoryInstructions.cs | 98 +- .../Instructions/NullCoalescingInstruction.cs | 24 +- .../IL/Instructions/NullableInstructions.cs | 24 +- .../IL/Instructions/SimpleInstruction.cs | 82 +- .../IL/Instructions/StringToInt.cs | 42 +- .../IL/Instructions/SwitchInstruction.cs | 84 +- .../IL/Instructions/TryInstruction.cs | 94 +- .../IL/Instructions/UnaryInstruction.cs | 24 +- .../IL/Instructions/UsingInstruction.cs | 42 +- 31 files changed, 1082 insertions(+), 1673 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index b29c7e7456..314f6f1988 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -370,18 +370,13 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.argument.WriteTo(output, options); + output.Write(')'); } } } @@ -471,20 +466,15 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.left.WriteTo(output, options); - output.Write(", "); - this.right.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.left.WriteTo(output, options); + output.Write(", "); + this.right.WriteTo(output, options); + output.Write(')'); } } } @@ -634,20 +624,15 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.target.WriteTo(output, options); - output.Write(", "); - this.value.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.target.WriteTo(output, options); + output.Write(", "); + this.value.WriteTo(output, options); + output.Write(')'); } } } @@ -1024,22 +1009,17 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayWriteLocals; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - variable.WriteTo(output); - output.Write('('); - this.init.WriteTo(output, options); - output.Write(", "); - this.body.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + variable.WriteTo(output); + output.Write('('); + this.init.WriteTo(output, options); + output.Write(", "); + this.body.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -2359,17 +2339,12 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayReadLocals; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - variable.WriteTo(output); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + variable.WriteTo(output); } public override void AcceptVisitor(ILVisitor visitor) { @@ -2438,17 +2413,12 @@ protected override void Disconnected() base.Disconnected(); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - variable.WriteTo(output); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + variable.WriteTo(output); } public override void AcceptVisitor(ILVisitor visitor) { @@ -2578,20 +2548,15 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayWriteLocals; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - variable.WriteTo(output); - output.Write('('); - this.value.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + variable.WriteTo(output); + output.Write('('); + this.value.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -2688,20 +2653,15 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - this.value.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + this.value.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -2858,17 +2818,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); } public override void AcceptVisitor(ILVisitor visitor) { @@ -2900,17 +2855,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); } public override void AcceptVisitor(ILVisitor visitor) { @@ -2942,17 +2892,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); } public override void AcceptVisitor(ILVisitor visitor) { @@ -2984,17 +2929,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3026,17 +2966,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3068,17 +3003,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3110,17 +3040,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - Disassembler.DisassemblerHelpers.WriteOperand(output, Value); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + Disassembler.DisassemblerHelpers.WriteOperand(output, Value); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3182,20 +3107,15 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (method != null) { - WriteILRange(output, options); - output.Write(OpCode); - if (method != null) - { - output.Write(' '); - method.WriteTo(output); - } + output.Write(' '); + method.WriteTo(output); } - finally { output.MarkNodeEnd(this); } } public override void AcceptVisitor(ILVisitor visitor) { @@ -3238,23 +3158,18 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (method != null) { - WriteILRange(output, options); - output.Write(OpCode); - if (method != null) - { - output.Write(' '); - method.WriteTo(output); - } - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.Write(' '); + method.WriteTo(output); } - finally { output.MarkNodeEnd(this); } + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3304,25 +3219,20 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + if (method != null) { - WriteILRange(output, options); - output.Write(OpCode); output.Write(' '); - type.WriteTo(output); - if (method != null) - { - output.Write(' '); - method.WriteTo(output); - } - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + method.WriteTo(output); + } + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3359,17 +3269,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3403,17 +3308,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - member.WriteTo(output); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + member.WriteTo(output); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3496,20 +3396,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3639,26 +3534,21 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - if (IsVolatile) - output.Write("volatile."); - if (UnalignedPrefix > 0) - output.Write("unaligned(" + UnalignedPrefix + ")."); - output.Write(OpCode); - output.Write('('); - this.destAddress.WriteTo(output, options); - output.Write(", "); - this.sourceAddress.WriteTo(output, options); - output.Write(", "); - this.size.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + if (IsVolatile) + output.Write("volatile."); + if (UnalignedPrefix > 0) + output.Write("unaligned(" + UnalignedPrefix + ")."); + output.Write(OpCode); + output.Write('('); + this.destAddress.WriteTo(output, options); + output.Write(", "); + this.sourceAddress.WriteTo(output, options); + output.Write(", "); + this.size.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3795,26 +3685,21 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - if (IsVolatile) - output.Write("volatile."); - if (UnalignedPrefix > 0) - output.Write("unaligned(" + UnalignedPrefix + ")."); - output.Write(OpCode); - output.Write('('); - this.address.WriteTo(output, options); - output.Write(", "); - this.value.WriteTo(output, options); - output.Write(", "); - this.size.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + if (IsVolatile) + output.Write("volatile."); + if (UnalignedPrefix > 0) + output.Write("unaligned(" + UnalignedPrefix + ")."); + output.Write(OpCode); + output.Write('('); + this.address.WriteTo(output, options); + output.Write(", "); + this.value.WriteTo(output, options); + output.Write(", "); + this.size.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3916,22 +3801,17 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - if (DelayExceptions) - output.Write("delayex."); - output.Write(OpCode); - output.Write(' '); - @field.WriteTo(output); - output.Write('('); - this.target.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + if (DelayExceptions) + output.Write("delayex."); + output.Write(OpCode); + output.Write(' '); + @field.WriteTo(output); + output.Write('('); + this.target.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -3965,17 +3845,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - @field.WriteTo(output); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + @field.WriteTo(output); } public override void AcceptVisitor(ILVisitor visitor) { @@ -4021,20 +3896,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -4071,20 +3941,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -4299,20 +4164,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - this.target.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + this.target.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -4495,20 +4355,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -4554,20 +4409,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -4613,20 +4463,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -4735,28 +4580,23 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + bool first = true; + foreach (var indices in Indices) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - bool first = true; - foreach (var indices in Indices) - { - if (!first) - output.Write(", "); - else - first = false; - indices.WriteTo(output, options); - } - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + if (!first) + output.Write(", "); + else + first = false; + indices.WriteTo(output, options); + } + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -4793,17 +4633,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); } public override void AcceptVisitor(ILVisitor visitor) { @@ -4914,17 +4749,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); } public override void AcceptVisitor(ILVisitor visitor) { @@ -5120,31 +4950,26 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + if (WithSystemIndex) + output.Write("withsystemindex."); + if (DelayExceptions) + output.Write("delayex."); + if (IsReadOnly) + output.Write("readonly."); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + this.array.WriteTo(output, options); + foreach (var indices in Indices) { - WriteILRange(output, options); - if (WithSystemIndex) - output.Write("withsystemindex."); - if (DelayExceptions) - output.Write("delayex."); - if (IsReadOnly) - output.Write("readonly."); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - this.array.WriteTo(output, options); - foreach (var indices in Indices) - { - output.Write(", "); - indices.WriteTo(output, options); - } - output.Write(')'); + output.Write(", "); + indices.WriteTo(output, options); } - finally { output.MarkNodeEnd(this); } + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -5250,27 +5075,22 @@ public override InstructionFlags DirectFlags { return InstructionFlags.MayThrow; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + if (IsReadOnly) + output.Write("readonly."); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + this.array.WriteTo(output, options); + foreach (var indices in Indices) { - WriteILRange(output, options); - if (IsReadOnly) - output.Write("readonly."); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - this.array.WriteTo(output, options); - foreach (var indices in Indices) - { - output.Write(", "); - indices.WriteTo(output, options); - } - output.Write(')'); + output.Write(", "); + indices.WriteTo(output, options); } - finally { output.MarkNodeEnd(this); } + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -5368,23 +5188,18 @@ public override InstructionFlags DirectFlags { return InstructionFlags.None; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (method != null) { - WriteILRange(output, options); - output.Write(OpCode); - if (method != null) - { - output.Write(' '); - method.WriteTo(output); - } - output.Write('('); - this.argument.WriteTo(output, options); - output.Write(')'); + output.Write(' '); + method.WriteTo(output); } - finally { output.MarkNodeEnd(this); } + output.Write('('); + this.argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -5621,25 +5436,20 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (method != null) { - WriteILRange(output, options); - output.Write(OpCode); - if (method != null) - { - output.Write(' '); - method.WriteTo(output); - } - output.Write('('); - this.left.WriteTo(output, options); - output.Write(", "); - this.right.WriteTo(output, options); - output.Write(')'); + output.Write(' '); + method.WriteTo(output); } - finally { output.MarkNodeEnd(this); } + output.Write('('); + this.left.WriteTo(output, options); + output.Write(", "); + this.right.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -6823,20 +6633,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -6910,20 +6715,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -7013,18 +6813,13 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.value.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.value.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -7114,18 +6909,13 @@ public override InstructionFlags DirectFlags { return InstructionFlags.SideEffect; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - this.value.WriteTo(output, options); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + this.value.WriteTo(output, options); + output.Write(')'); } public override void AcceptVisitor(ILVisitor visitor) { @@ -7239,17 +7029,12 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('('); - output.Write(')'); - } - finally { output.MarkNodeEnd(this); } + WriteILRange(output, options); + output.Write(OpCode); + output.Write('('); + output.Write(')'); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index 1035722357..96fcb35ef5 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -429,12 +429,8 @@ namespace <#=opCode.Namespace#> void OriginalWriteTo(ITextOutput output, ILAstWritingOptions options) {<#=Body(opCode.WriteToBody)#>} <# } else { #> - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) - { - output.MarkNodeStart(this); - try {<#=Body(opCode.WriteToBody)#>} - finally { output.MarkNodeEnd(this); } - } + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) + {<#=Body(opCode.WriteToBody)#>} <# } #> <# } #> <# if (opCode.GenerateAcceptVisitor) { #> diff --git a/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs index ca36302f03..b3499fb9ae 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/BinaryNumericInstruction.cs @@ -184,42 +184,34 @@ internal static string GetOperatorName(BinaryNumericOperator @operator) } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + output.Write("." + GetOperatorName(Operator)); + if (CheckForOverflow) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write("." + GetOperatorName(Operator)); - if (CheckForOverflow) - { - output.Write(".ovf"); - } - if (Sign == Sign.Unsigned) - { - output.Write(".unsigned"); - } - else if (Sign == Sign.Signed) - { - output.Write(".signed"); - } - output.Write('.'); - output.Write(resultType.ToString().ToLowerInvariant()); - if (IsLifted) - { - output.Write(".lifted"); - } - output.Write('('); - Left.WriteTo(output, options); - output.Write(", "); - Right.WriteTo(output, options); - output.Write(')'); + output.Write(".ovf"); + } + if (Sign == Sign.Unsigned) + { + output.Write(".unsigned"); + } + else if (Sign == Sign.Signed) + { + output.Write(".signed"); } - finally + output.Write('.'); + output.Write(resultType.ToString().ToLowerInvariant()); + if (IsLifted) { - output.MarkNodeEnd(this); + output.Write(".lifted"); } + output.Write('('); + Left.WriteTo(output, options); + output.Write(", "); + Right.WriteTo(output, options); + output.Write(')'); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/Block.cs b/ICSharpCode.Decompiler/IL/Instructions/Block.cs index c6aae0f1bd..6c8206c46b 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Block.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Block.cs @@ -258,47 +258,39 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write("Block "); + output.WriteLocalReference(Label, this, isDefinition: true); + if (Kind != BlockKind.ControlFlow) + output.Write($" ({Kind})"); + if (Parent is BlockContainer) + output.Write(" (incoming: {0})", IncomingEdgeCount); + output.Write(' '); + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + int index = 0; + foreach (var inst in Instructions) { - WriteILRange(output, options); - output.Write("Block "); - output.WriteLocalReference(Label, this, isDefinition: true); - if (Kind != BlockKind.ControlFlow) - output.Write($" ({Kind})"); - if (Parent is BlockContainer) - output.Write(" (incoming: {0})", IncomingEdgeCount); - output.Write(' '); - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - int index = 0; - foreach (var inst in Instructions) - { - if (options.ShowChildIndexInBlock) - { - output.Write("[" + index + "] "); - index++; - } - inst.WriteTo(output, options); - output.WriteLine(); - } - if (finalInstruction.OpCode != OpCode.Nop) + if (options.ShowChildIndexInBlock) { - output.Write("final: "); - finalInstruction.WriteTo(output, options); - output.WriteLine(); + output.Write("[" + index + "] "); + index++; } - output.Unindent(); - output.Write("}"); - output.MarkFoldEnd(); + inst.WriteTo(output, options); + output.WriteLine(); } - finally + if (finalInstruction.OpCode != OpCode.Nop) { - output.MarkNodeEnd(this); + output.Write("final: "); + finalInstruction.WriteTo(output, options); + output.WriteLine(); } + output.Unindent(); + output.Write("}"); + output.MarkFoldEnd(); } protected override int GetChildCount() diff --git a/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs b/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs index fc341879b4..9c212982d5 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs @@ -125,57 +125,49 @@ protected override void Disconnected() entryPoint.IncomingEdgeCount--; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.WriteLocalReference("BlockContainer", this, isDefinition: true); + output.Write(' '); + switch (Kind) + { + case ContainerKind.Loop: + output.Write("(while-true) "); + break; + case ContainerKind.Switch: + output.Write("(switch) "); + break; + case ContainerKind.While: + output.Write("(while) "); + break; + case ContainerKind.DoWhile: + output.Write("(do-while) "); + break; + case ContainerKind.For: + output.Write("(for) "); + break; + } + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + foreach (var inst in Blocks) { - WriteILRange(output, options); - output.WriteLocalReference("BlockContainer", this, isDefinition: true); - output.Write(' '); - switch (Kind) + if (inst.Parent == this) { - case ContainerKind.Loop: - output.Write("(while-true) "); - break; - case ContainerKind.Switch: - output.Write("(switch) "); - break; - case ContainerKind.While: - output.Write("(while) "); - break; - case ContainerKind.DoWhile: - output.Write("(do-while) "); - break; - case ContainerKind.For: - output.Write("(for) "); - break; + inst.WriteTo(output, options); } - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - foreach (var inst in Blocks) + else { - if (inst.Parent == this) - { - inst.WriteTo(output, options); - } - else - { - output.Write("stale reference to "); - output.WriteLocalReference(inst.Label, inst); - } - output.WriteLine(); - output.WriteLine(); + output.Write("stale reference to "); + output.WriteLocalReference(inst.Label, inst); } - output.Unindent(); - output.Write("}"); - output.MarkFoldEnd(); - } - finally - { - output.MarkNodeEnd(this); + output.WriteLine(); + output.WriteLine(); } + output.Unindent(); + output.Write("}"); + output.MarkFoldEnd(); } protected override int GetChildCount() diff --git a/ICSharpCode.Decompiler/IL/Instructions/Branch.cs b/ICSharpCode.Decompiler/IL/Instructions/Branch.cs index a398d92617..9f5aee7455 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Branch.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Branch.cs @@ -118,20 +118,12 @@ internal override void CheckInvariant(ILPhase phase) } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - output.WriteLocalReference(TargetLabel, (object?)targetBlock ?? TargetILOffset); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + output.WriteLocalReference(TargetLabel, (object?)targetBlock ?? TargetILOffset); } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs b/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs index d4be2caf80..54a2b88805 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CallIndirect.cs @@ -74,36 +74,28 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write("call.indirect "); + FunctionPointerType.ReturnType.WriteTo(output); + output.Write('('); + functionPointer.WriteTo(output, options); + int firstArgument = IsInstance ? 1 : 0; + if (firstArgument == 1) { - WriteILRange(output, options); - output.Write("call.indirect "); - FunctionPointerType.ReturnType.WriteTo(output); - output.Write('('); - functionPointer.WriteTo(output, options); - int firstArgument = IsInstance ? 1 : 0; - if (firstArgument == 1) - { - output.Write(", "); - Arguments[0].WriteTo(output, options); - } - foreach (var (inst, type) in Arguments.Zip(FunctionPointerType.ParameterTypes, (a, b) => (a, b))) - { - output.Write(", "); - inst.WriteTo(output, options); - output.Write(" : "); - type.WriteTo(output); - } - if (Arguments.Count > 0) - output.Write(')'); + output.Write(", "); + Arguments[0].WriteTo(output, options); } - finally + foreach (var (inst, type) in Arguments.Zip(FunctionPointerType.ParameterTypes, (a, b) => (a, b))) { - output.MarkNodeEnd(this); + output.Write(", "); + inst.WriteTo(output, options); + output.Write(" : "); + type.WriteTo(output); } + if (Arguments.Count > 0) + output.Write(')'); } protected override int GetChildCount() diff --git a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs index 0a1025ff33..3aa46e16e7 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs @@ -138,36 +138,28 @@ internal override void CheckInvariant(ILPhase phase) } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + if (ConstrainedTo != null) { - WriteILRange(output, options); - if (ConstrainedTo != null) - { - output.Write("constrained["); - ConstrainedTo.WriteTo(output); - output.Write("]."); - } - if (IsTail) - output.Write("tail."); - output.Write(OpCode); - output.Write(' '); - Method.WriteTo(output); - output.Write('('); - for (int i = 0; i < Arguments.Count; i++) - { - if (i > 0) - output.Write(", "); - Arguments[i].WriteTo(output, options); - } - output.Write(')'); + output.Write("constrained["); + ConstrainedTo.WriteTo(output); + output.Write("]."); } - finally + if (IsTail) + output.Write("tail."); + output.Write(OpCode); + output.Write(' '); + Method.WriteTo(output); + output.Write('('); + for (int i = 0; i < Arguments.Count; i++) { - output.MarkNodeEnd(this); + if (i > 0) + output.Write(", "); + Arguments[i].WriteTo(output, options); } + output.Write(')'); } protected internal sealed override bool PerformMatch(ILInstruction? other, ref Patterns.Match match) diff --git a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs index 8e98e002ee..2a67566443 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Comp.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Comp.cs @@ -179,52 +179,44 @@ internal override void CheckInvariant(ILPhase phase) } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + if (options.UseLogicOperationSugar && MatchLogicNot(out var arg)) { - WriteILRange(output, options); - if (options.UseLogicOperationSugar && MatchLogicNot(out var arg)) - { - output.Write("logic.not("); - arg.WriteTo(output, options); - output.Write(')'); - return; - } - output.Write(OpCode); - output.Write('.'); - output.Write(InputType.ToString().ToLower()); - switch (Sign) - { - case Sign.Signed: - output.Write(".signed"); - break; - case Sign.Unsigned: - output.Write(".unsigned"); - break; - } - switch (LiftingKind) - { - case ComparisonLiftingKind.CSharp: - output.Write(".lifted[C#]"); - break; - case ComparisonLiftingKind.ThreeValuedLogic: - output.Write(".lifted[3VL]"); - break; - } - output.Write('('); - Left.WriteTo(output, options); - output.Write(' '); - output.Write(Kind.GetToken()); - output.Write(' '); - Right.WriteTo(output, options); + output.Write("logic.not("); + arg.WriteTo(output, options); output.Write(')'); + return; } - finally + output.Write(OpCode); + output.Write('.'); + output.Write(InputType.ToString().ToLower()); + switch (Sign) { - output.MarkNodeEnd(this); + case Sign.Signed: + output.Write(".signed"); + break; + case Sign.Unsigned: + output.Write(".unsigned"); + break; } + switch (LiftingKind) + { + case ComparisonLiftingKind.CSharp: + output.Write(".lifted[C#]"); + break; + case ComparisonLiftingKind.ThreeValuedLogic: + output.Write(".lifted[3VL]"); + break; + } + output.Write('('); + Left.WriteTo(output, options); + output.Write(' '); + output.Write(Kind.GetToken()); + output.Write(' '); + Right.WriteTo(output, options); + output.Write(')'); } public static Comp LogicNot(ILInstruction arg) diff --git a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs index f6cecb8d14..85f49aad6e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CompoundAssignmentInstruction.cs @@ -272,43 +272,35 @@ public override InstructionFlags DirectFlags { } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + output.Write("." + BinaryNumericInstruction.GetOperatorName(Operator)); + if (CheckForOverflow) { - WriteILRange(output, options); - output.Write(OpCode); - output.Write("." + BinaryNumericInstruction.GetOperatorName(Operator)); - if (CheckForOverflow) - { - output.Write(".ovf"); - } - if (Sign == Sign.Unsigned) - { - output.Write(".unsigned"); - } - else if (Sign == Sign.Signed) - { - output.Write(".signed"); - } - output.Write('.'); - output.Write(UnderlyingResultType.ToString().ToLowerInvariant()); - if (IsLifted) - { - output.Write(".lifted"); - } - base.WriteSuffix(output); - output.Write('('); - Target.WriteTo(output, options); - output.Write(", "); - Value.WriteTo(output, options); - output.Write(')'); + output.Write(".ovf"); + } + if (Sign == Sign.Unsigned) + { + output.Write(".unsigned"); } - finally + else if (Sign == Sign.Signed) { - output.MarkNodeEnd(this); + output.Write(".signed"); } + output.Write('.'); + output.Write(UnderlyingResultType.ToString().ToLowerInvariant()); + if (IsLifted) + { + output.Write(".lifted"); + } + base.WriteSuffix(output); + output.Write('('); + Target.WriteTo(output, options); + output.Write(", "); + Value.WriteTo(output, options); + output.Write(')'); } } @@ -344,26 +336,18 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - base.WriteSuffix(output); - output.Write(' '); - Method.WriteTo(output); - output.Write('('); - this.Target.WriteTo(output, options); - output.Write(", "); - this.Value.WriteTo(output, options); - output.Write(')'); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + base.WriteSuffix(output); + output.Write(' '); + Method.WriteTo(output); + output.Write('('); + this.Target.WriteTo(output, options); + output.Write(", "); + this.Value.WriteTo(output, options); + output.Write(')'); } } @@ -388,23 +372,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write("." + Operation.ToString().ToLower()); - DynamicInstruction.WriteBinderFlags(BinderFlags, output, options); - base.WriteSuffix(output); - output.Write(' '); - DynamicInstruction.WriteArgumentList(output, options, (Target, TargetArgumentInfo), (Value, ValueArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + output.Write("." + Operation.ToString().ToLower()); + DynamicInstruction.WriteBinderFlags(BinderFlags, output, options); + base.WriteSuffix(output); + output.Write(' '); + DynamicInstruction.WriteArgumentList(output, options, (Target, TargetArgumentInfo), (Value, ValueArgumentInfo)); } internal static bool IsExpressionTypeSupported(ExpressionType type) diff --git a/ICSharpCode.Decompiler/IL/Instructions/Conv.cs b/ICSharpCode.Decompiler/IL/Instructions/Conv.cs index d5fe091fd2..e029b18947 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Conv.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Conv.cs @@ -314,54 +314,46 @@ public StackType UnderlyingResultType { get => TargetType.GetStackType(); } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (CheckForOverflow) + { + output.Write(".ovf"); + } + if (InputSign == Sign.Unsigned) + { + output.Write(".unsigned"); + } + else if (InputSign == Sign.Signed) + { + output.Write(".signed"); + } + if (IsLifted) { - WriteILRange(output, options); - output.Write(OpCode); - if (CheckForOverflow) - { - output.Write(".ovf"); - } - if (InputSign == Sign.Unsigned) - { - output.Write(".unsigned"); - } - else if (InputSign == Sign.Signed) - { - output.Write(".signed"); - } - if (IsLifted) - { - output.Write(".lifted"); - } - output.Write(' '); - output.Write(InputType); - output.Write("->"); - output.Write(TargetType); - output.Write(' '); - switch (Kind) - { - case ConversionKind.SignExtend: - output.Write(""); - break; - case ConversionKind.ZeroExtend: - output.Write(""); - break; - case ConversionKind.Invalid: - output.Write(""); - break; - } - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); + output.Write(".lifted"); } - finally + output.Write(' '); + output.Write(InputType); + output.Write("->"); + output.Write(TargetType); + output.Write(' '); + switch (Kind) { - output.MarkNodeEnd(this); + case ConversionKind.SignExtend: + output.Write(""); + break; + case ConversionKind.ZeroExtend: + output.Write(""); + break; + case ConversionKind.Invalid: + output.Write(""); + break; } + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } protected override InstructionFlags ComputeFlags() diff --git a/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs index d02f1824c7..f2993a7f84 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DeconstructInstruction.cs @@ -159,43 +159,35 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write("deconstruct "); + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + output.WriteLine("init:"); + output.Indent(); + foreach (var inst in this.Init) { - WriteILRange(output, options); - output.Write("deconstruct "); - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - output.WriteLine("init:"); - output.Indent(); - foreach (var inst in this.Init) - { - inst.WriteTo(output, options); - output.WriteLine(); - } - output.Unindent(); - output.WriteLine("pattern:"); - output.Indent(); - pattern.WriteTo(output, options); - output.Unindent(); - output.WriteLine(); - output.Write("conversions: "); - conversions.WriteTo(output, options); + inst.WriteTo(output, options); output.WriteLine(); - output.Write("assignments: "); - assignments.WriteTo(output, options); - output.Unindent(); - output.WriteLine(); - output.Write('}'); - output.MarkFoldEnd(); - } - finally - { - output.MarkNodeEnd(this); } + output.Unindent(); + output.WriteLine("pattern:"); + output.Indent(); + pattern.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write("conversions: "); + conversions.WriteTo(output, options); + output.WriteLine(); + output.Write("assignments: "); + assignments.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write('}'); + output.MarkFoldEnd(); } internal static bool IsConversionStLoc(ILInstruction inst, out ILVariable variable, out ILVariable inputVariable) diff --git a/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs index f3f1718154..608aa4483c 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DeconstructResultInstruction.cs @@ -37,23 +37,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write(' '); - output.Write(Index.ToString()); - output.Write('('); - this.Argument.WriteTo(output, options); - output.Write(')'); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + output.Write(Index.ToString()); + output.Write('('); + this.Argument.WriteTo(output, options); + output.Write(')'); } MatchInstruction? FindMatch() diff --git a/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs b/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs index 568a315170..3e037a336e 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/DynamicInstructions.cs @@ -130,24 +130,16 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - argument.WriteTo(output, options); - output.Write(')'); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + argument.WriteTo(output, options); + output.Write(')'); } public DynamicConvertInstruction(CSharpBinderFlags binderFlags, IType type, IType? context, ILInstruction argument) @@ -190,35 +182,27 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Name); + if (TypeArguments.Count > 0) { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Name); - if (TypeArguments.Count > 0) + output.Write('<'); + int i = 0; + foreach (var typeArg in TypeArguments) { - output.Write('<'); - int i = 0; - foreach (var typeArg in TypeArguments) - { - if (i > 0) - output.Write(", "); - typeArg.WriteTo(output); - i++; - } - output.Write('>'); + if (i > 0) + output.Write(", "); + typeArg.WriteTo(output); + i++; } - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); + output.Write('>'); } + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); } public override StackType ResultType => StackType.O; @@ -244,22 +228,14 @@ public DynamicGetMemberInstruction(CSharpBinderFlags binderFlags, string? name, Target = target; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Name); - WriteArgumentList(output, options, (Target, TargetArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Name); + WriteArgumentList(output, options, (Target, TargetArgumentInfo)); } public override StackType ResultType => StackType.O; @@ -288,22 +264,14 @@ public DynamicSetMemberInstruction(CSharpBinderFlags binderFlags, string? name, Value = value; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Name); - WriteArgumentList(output, options, (Target, TargetArgumentInfo), (Value, ValueArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Name); + WriteArgumentList(output, options, (Target, TargetArgumentInfo), (Value, ValueArgumentInfo)); } public override StackType ResultType => StackType.O; @@ -334,22 +302,14 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write("get_Item"); - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write("get_Item"); + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); } public override StackType ResultType => StackType.O; @@ -374,22 +334,14 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write("set_Item"); - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write("set_Item"); + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); } public override StackType ResultType => StackType.O; @@ -417,23 +369,15 @@ public DynamicInvokeConstructorInstruction(CSharpBinderFlags binderFlags, IType? this.resultType = type; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - resultType?.WriteTo(output); - output.Write(".ctor"); - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + resultType?.WriteTo(output); + output.Write(".ctor"); + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); } public override StackType ResultType => resultType?.GetStackType() ?? StackType.Unknown; @@ -462,22 +406,14 @@ public DynamicBinaryOperatorInstruction(CSharpBinderFlags binderFlags, Expressio Right = right; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Operation.ToString()); - WriteArgumentList(output, options, (Left, LeftArgumentInfo), (Right, RightArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Operation.ToString()); + WriteArgumentList(output, options, (Left, LeftArgumentInfo), (Right, RightArgumentInfo)); } public override StackType ResultType => StackType.O; @@ -512,22 +448,14 @@ public DynamicLogicOperatorInstruction(CSharpBinderFlags binderFlags, Expression Right = right; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Operation.ToString()); - WriteArgumentList(output, options, (Left, LeftArgumentInfo), (Right, RightArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Operation.ToString()); + WriteArgumentList(output, options, (Left, LeftArgumentInfo), (Right, RightArgumentInfo)); } public override StackType ResultType => StackType.O; @@ -567,22 +495,14 @@ public DynamicUnaryOperatorInstruction(CSharpBinderFlags binderFlags, Expression Operand = operand; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write(Operation.ToString()); - WriteArgumentList(output, options, (Operand, OperandArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write(Operation.ToString()); + WriteArgumentList(output, options, (Operand, OperandArgumentInfo)); } public override StackType ResultType { @@ -622,21 +542,13 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo)); } public override StackType ResultType => StackType.O; @@ -660,23 +572,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - WriteBinderFlags(output, options); - output.Write(' '); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + WriteBinderFlags(output, options); + output.Write(' '); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override StackType ResultType => StackType.I4; diff --git a/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs b/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs index 4434ae5caf..670ebe6531 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ExpressionTreeCast.cs @@ -14,25 +14,17 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - if (IsChecked) - output.Write(".checked"); - output.Write(' '); - type.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + if (IsChecked) + output.Write(".checked"); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs b/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs index 97e601dd93..762d57e8a6 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/ILFunction.cs @@ -270,93 +270,85 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (Method != null) { - WriteILRange(output, options); - output.Write(OpCode); - if (Method != null) - { - output.Write(' '); - Method.WriteTo(output); - } - switch (kind) - { - case ILFunctionKind.ExpressionTree: - output.Write(".ET"); - break; - case ILFunctionKind.LocalFunction: - output.Write(".local"); - break; - } - if (DelegateType != null) - { - output.Write("["); - DelegateType.WriteTo(output); - output.Write("]"); - } - output.WriteLine(" {"); - output.Indent(); + output.Write(' '); + Method.WriteTo(output); + } + switch (kind) + { + case ILFunctionKind.ExpressionTree: + output.Write(".ET"); + break; + case ILFunctionKind.LocalFunction: + output.Write(".local"); + break; + } + if (DelegateType != null) + { + output.Write("["); + DelegateType.WriteTo(output); + output.Write("]"); + } + output.WriteLine(" {"); + output.Indent(); - if (IsAsync) - { - output.WriteLine(".async"); - } - if (IsIterator) - { - output.WriteLine(".iterator"); - } - if (DeclarationScope != null) - { - output.Write("declared as " + Name + " in "); - output.WriteLocalReference(DeclarationScope.EntryPoint.Label, DeclarationScope); - output.WriteLine(); - } + if (IsAsync) + { + output.WriteLine(".async"); + } + if (IsIterator) + { + output.WriteLine(".iterator"); + } + if (DeclarationScope != null) + { + output.Write("declared as " + Name + " in "); + output.WriteLocalReference(DeclarationScope.EntryPoint.Label, DeclarationScope); + output.WriteLine(); + } - output.MarkFoldStart(Variables.Count + " variable(s)", true); - foreach (var variable in Variables) - { - variable.WriteDefinitionTo(output); - output.WriteLine(); - } - output.MarkFoldEnd(); + output.MarkFoldStart(Variables.Count + " variable(s)", true); + foreach (var variable in Variables) + { + variable.WriteDefinitionTo(output); output.WriteLine(); + } + output.MarkFoldEnd(); + output.WriteLine(); - foreach (string warning in Warnings) - { - output.WriteLine("//" + warning); - } + foreach (string warning in Warnings) + { + output.WriteLine("//" + warning); + } - body.WriteTo(output, options); + body.WriteTo(output, options); + output.WriteLine(); + + foreach (var localFunction in LocalFunctions) + { output.WriteLine(); + localFunction.WriteTo(output, options); + } - foreach (var localFunction in LocalFunctions) + if (options.ShowILRanges) + { + var unusedILRanges = FindUnusedILRanges(); + if (!unusedILRanges.IsEmpty) { + output.Write("// Unused IL Ranges: "); + output.Write(string.Join(", ", unusedILRanges.Intervals.Select( + range => $"[{range.Start:x4}..{range.InclusiveEnd:x4}]"))); output.WriteLine(); - localFunction.WriteTo(output, options); } - - if (options.ShowILRanges) - { - var unusedILRanges = FindUnusedILRanges(); - if (!unusedILRanges.IsEmpty) - { - output.Write("// Unused IL Ranges: "); - output.Write(string.Join(", ", unusedILRanges.Intervals.Select( - range => $"[{range.Start:x4}..{range.InclusiveEnd:x4}]"))); - output.WriteLine(); - } - } - - output.Unindent(); - output.WriteLine("}"); - } - finally - { - output.MarkNodeEnd(this); } + + output.Unindent(); + output.WriteLine("}"); } LongSet FindUnusedILRanges() 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 7328beee06..f61e8b510c 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/IfInstruction.cs @@ -82,47 +82,39 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + if (options.UseLogicOperationSugar) { - WriteILRange(output, options); - if (options.UseLogicOperationSugar) + if (MatchLogicAnd(out var lhs, out var rhs)) { - if (MatchLogicAnd(out var lhs, out var rhs)) - { - output.Write("logic.and("); - lhs.WriteTo(output, options); - output.Write(", "); - rhs.WriteTo(output, options); - output.Write(')'); - return; - } - if (MatchLogicOr(out lhs, out rhs)) - { - output.Write("logic.or("); - lhs.WriteTo(output, options); - output.Write(", "); - rhs.WriteTo(output, options); - output.Write(')'); - return; - } + output.Write("logic.and("); + lhs.WriteTo(output, options); + output.Write(", "); + rhs.WriteTo(output, options); + output.Write(')'); + return; } - output.Write(OpCode); - output.Write(" ("); - condition.WriteTo(output, options); - output.Write(") "); - trueInst.WriteTo(output, options); - if (falseInst.OpCode != OpCode.Nop) + if (MatchLogicOr(out lhs, out rhs)) { - output.Write(" else "); - falseInst.WriteTo(output, options); + output.Write("logic.or("); + lhs.WriteTo(output, options); + output.Write(", "); + rhs.WriteTo(output, options); + output.Write(')'); + return; } } - finally + output.Write(OpCode); + output.Write(" ("); + condition.WriteTo(output, options); + output.Write(") "); + trueInst.WriteTo(output, options); + if (falseInst.OpCode != OpCode.Nop) { - output.MarkNodeEnd(this); + output.Write(" else "); + falseInst.WriteTo(output, options); } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs b/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs index f52c4a20d1..7a3603d5c8 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/LdLen.cs @@ -39,23 +39,15 @@ public override StackType ResultType { get { return resultType; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write('.'); - output.Write(resultType); - output.Write('('); - this.array.WriteTo(output, options); - output.Write(')'); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + output.Write('.'); + output.Write(resultType); + output.Write('('); + this.array.WriteTo(output, options); + output.Write(')'); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/Leave.cs b/ICSharpCode.Decompiler/IL/Instructions/Leave.cs index bd426503f4..4d231e9b38 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Leave.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Leave.cs @@ -114,25 +114,17 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - if (targetContainer != null) - { - output.Write(' '); - output.WriteLocalReference(TargetLabel, targetContainer); - output.Write(" ("); - value.WriteTo(output, options); - output.Write(')'); - } - } - finally + WriteILRange(output, options); + output.Write(OpCode); + if (targetContainer != null) { - output.MarkNodeEnd(this); + output.Write(' '); + output.WriteLocalReference(TargetLabel, targetContainer); + output.Write(" ("); + value.WriteTo(output, options); + output.Write(')'); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs index 29d9c5f0a1..13bfe9afc3 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/LockInstruction.cs @@ -27,25 +27,17 @@ namespace ICSharpCode.Decompiler.IL { partial class LockInstruction { - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write("lock ("); - OnExpression.WriteTo(output, options); - output.WriteLine(") {"); - output.Indent(); - Body.WriteTo(output, options); - output.Unindent(); - output.WriteLine(); - output.Write("}"); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write("lock ("); + OnExpression.WriteTo(output, options); + output.WriteLine(") {"); + output.Indent(); + Body.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write("}"); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs index ede8f0569a..899b7de9aa 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs @@ -274,60 +274,52 @@ internal static bool IsDeconstructMethod(IMethod? method) return true; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (CheckNotNull) { - WriteILRange(output, options); - output.Write(OpCode); - if (CheckNotNull) - { - output.Write(".notnull"); - } - if (CheckType) - { - output.Write(".type["); - variable.Type.WriteTo(output); - output.Write(']'); - } - if (IsDeconstructCall) - { - output.Write(".deconstruct["); - if (method == null) - output.Write(""); - else - method.WriteTo(output); - output.Write(']'); - } - if (IsDeconstructTuple) - { - output.Write(".tuple"); - } - output.Write(' '); - output.Write('('); - Variable.WriteTo(output); - output.Write(" = "); - TestedOperand.WriteTo(output, options); - output.Write(')'); - if (SubPatterns.Count > 0) - { - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - foreach (var pattern in SubPatterns) - { - pattern.WriteTo(output, options); - output.WriteLine(); - } - output.Unindent(); - output.Write('}'); - output.MarkFoldEnd(); - } + output.Write(".notnull"); + } + if (CheckType) + { + output.Write(".type["); + variable.Type.WriteTo(output); + output.Write(']'); } - finally + if (IsDeconstructCall) { - output.MarkNodeEnd(this); + output.Write(".deconstruct["); + if (method == null) + output.Write(""); + else + method.WriteTo(output); + output.Write(']'); + } + if (IsDeconstructTuple) + { + output.Write(".tuple"); + } + output.Write(' '); + output.Write('('); + Variable.WriteTo(output); + output.Write(" = "); + TestedOperand.WriteTo(output, options); + output.Write(')'); + if (SubPatterns.Count > 0) + { + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + foreach (var pattern in SubPatterns) + { + pattern.WriteTo(output, options); + output.WriteLine(); + } + output.Unindent(); + output.Write('}'); + output.MarkFoldEnd(); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs b/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs index 75c7b191a7..fe4d687f17 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/MemoryInstructions.cs @@ -37,78 +37,62 @@ interface ISupportsVolatilePrefix partial class LdObj { - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + if (options.UseFieldSugar) { - if (options.UseFieldSugar) + if (this.MatchLdFld(out var target, out var field)) { - if (this.MatchLdFld(out var target, out var field)) - { - WriteILRange(output, options); - output.Write("ldfld "); - field.WriteTo(output); - output.Write('('); - target.WriteTo(output, options); - output.Write(')'); - return; - } - else if (this.MatchLdsFld(out field)) - { - WriteILRange(output, options); - output.Write("ldsfld "); - field.WriteTo(output); - return; - } + WriteILRange(output, options); + output.Write("ldfld "); + field.WriteTo(output); + output.Write('('); + target.WriteTo(output, options); + output.Write(')'); + return; + } + else if (this.MatchLdsFld(out field)) + { + WriteILRange(output, options); + output.Write("ldsfld "); + field.WriteTo(output); + return; } - OriginalWriteTo(output, options); - } - finally - { - output.MarkNodeEnd(this); } + OriginalWriteTo(output, options); } } partial class StObj { - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + if (options.UseFieldSugar) { - if (options.UseFieldSugar) + if (this.MatchStFld(out var target, out var field, out var value)) { - if (this.MatchStFld(out var target, out var field, out var value)) - { - WriteILRange(output, options); - output.Write("stfld "); - field.WriteTo(output); - output.Write('('); - target.WriteTo(output, options); - output.Write(", "); - value.WriteTo(output, options); - output.Write(')'); - return; - } - else if (this.MatchStsFld(out field, out value)) - { - WriteILRange(output, options); - output.Write("stsfld "); - field.WriteTo(output); - output.Write('('); - value.WriteTo(output, options); - output.Write(')'); - return; - } + WriteILRange(output, options); + output.Write("stfld "); + field.WriteTo(output); + output.Write('('); + target.WriteTo(output, options); + output.Write(", "); + value.WriteTo(output, options); + output.Write(')'); + return; + } + else if (this.MatchStsFld(out field, out value)) + { + WriteILRange(output, options); + output.Write("stsfld "); + field.WriteTo(output); + output.Write('('); + value.WriteTo(output, options); + output.Write(')'); + return; } - OriginalWriteTo(output, options); - } - finally - { - output.MarkNodeEnd(this); } + OriginalWriteTo(output, options); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs index b8c8f7d3e7..67085e0406 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/NullCoalescingInstruction.cs @@ -90,23 +90,15 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - output.Write("("); - valueInst.WriteTo(output, options); - output.Write(", "); - fallbackInst.WriteTo(output, options); - output.Write(")"); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + output.Write("("); + valueInst.WriteTo(output, options); + output.Write(", "); + fallbackInst.WriteTo(output, options); + output.Write(")"); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs b/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs index ab60c94931..8fc20f6a3a 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/NullableInstructions.cs @@ -90,25 +90,17 @@ 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.MarkNodeStart(this); - try + output.Write("nullable.unwrap."); + if (RefInput) { - output.Write("nullable.unwrap."); - if (RefInput) - { - output.Write("refinput."); - } - output.Write(ResultType); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(')'); - } - finally - { - output.MarkNodeEnd(this); + output.Write("refinput."); } + output.Write(ResultType); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(')'); } public override StackType ResultType { get; } diff --git a/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs index 3b58211997..661c999fae 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/SimpleInstruction.cs @@ -24,19 +24,11 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - // the non-custom WriteTo would add useless parentheses - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(OpCode); + // the non-custom WriteTo would add useless parentheses } } @@ -52,25 +44,17 @@ partial class Nop public NopKind Kind; - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (Kind != NopKind.Normal) { - WriteILRange(output, options); - output.Write(OpCode); - if (Kind != NopKind.Normal) - { - output.Write("." + Kind.ToString().ToLowerInvariant()); - } - if (!string.IsNullOrEmpty(Comment)) - { - output.Write(" // " + Comment); - } + output.Write("." + Kind.ToString().ToLowerInvariant()); } - finally + if (!string.IsNullOrEmpty(Comment)) { - output.MarkNodeEnd(this); + output.Write(" // " + Comment); } } } @@ -89,23 +73,15 @@ public override StackType ResultType { get { return ExpectedResultType; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (!string.IsNullOrEmpty(Message)) { - WriteILRange(output, options); - output.Write(OpCode); - if (!string.IsNullOrEmpty(Message)) - { - output.Write("(\""); - output.Write(Message); - output.Write("\")"); - } - } - finally - { - output.MarkNodeEnd(this); + output.Write("(\""); + output.Write(Message); + output.Write("\")"); } } } @@ -125,23 +101,15 @@ public override StackType ResultType { get { return ExpectedResultType; } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(OpCode); - if (!string.IsNullOrEmpty(Message)) - { - output.Write("(\""); - output.Write(Message); - output.Write("\")"); - } - } - finally + WriteILRange(output, options); + output.Write(OpCode); + if (!string.IsNullOrEmpty(Message)) { - output.MarkNodeEnd(this); + output.Write("(\""); + output.Write(Message); + output.Write("\")"); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs b/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs index 8802b8d647..c095665f75 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/StringToInt.cs @@ -52,34 +52,26 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write("string.to.int "); + ExpectedType.WriteTo(output); + output.Write('('); + Argument.WriteTo(output, options); + output.Write(", { "); + int i = 0; + foreach (var entry in Map) { - WriteILRange(output, options); - output.Write("string.to.int "); - ExpectedType.WriteTo(output); - output.Write('('); - Argument.WriteTo(output, options); - output.Write(", { "); - int i = 0; - foreach (var entry in Map) - { - if (i > 0) - output.Write(", "); - if (entry.Key is null) - output.Write($"[null] = {entry.Value}"); - else - output.Write($"[\"{entry.Key}\"] = {entry.Value}"); - i++; - } - output.Write(" })"); - } - finally - { - output.MarkNodeEnd(this); + if (i > 0) + output.Write(", "); + if (entry.Key is null) + output.Write($"[null] = {entry.Value}"); + else + output.Write($"[\"{entry.Key}\"] = {entry.Value}"); + i++; } + output.Write(" })"); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs index fdfb2bed66..7f4874a7d5 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/SwitchInstruction.cs @@ -83,36 +83,28 @@ public override InstructionFlags DirectFlags { } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write("switch"); + if (IsLifted) + output.Write(".lifted"); + output.Write(' '); + Type?.WriteTo(output); + output.Write('('); + value.WriteTo(output, options); + output.Write(") "); + output.MarkFoldStart("{...}"); + output.WriteLine("{"); + output.Indent(); + foreach (var section in this.Sections) { - WriteILRange(output, options); - output.Write("switch"); - if (IsLifted) - output.Write(".lifted"); - output.Write(' '); - Type?.WriteTo(output); - output.Write('('); - value.WriteTo(output, options); - output.Write(") "); - output.MarkFoldStart("{...}"); - output.WriteLine("{"); - output.Indent(); - foreach (var section in this.Sections) - { - section.WriteTo(output, options); - output.WriteLine(); - } - output.Unindent(); - output.Write('}'); - output.MarkFoldEnd(); - } - finally - { - output.MarkNodeEnd(this); + section.WriteTo(output, options); + output.WriteLine(); } + output.Unindent(); + output.Write('}'); + output.MarkFoldEnd(); } protected override int GetChildCount() @@ -232,37 +224,29 @@ public override InstructionFlags DirectFlags { } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + if (IsCompilerGeneratedDefaultSection) + output.Write("generated."); + output.WriteLocalReference("case", this, isDefinition: true); + output.Write(' '); + if (HasNullLabel) { - WriteILRange(output, options); - if (IsCompilerGeneratedDefaultSection) - output.Write("generated."); - output.WriteLocalReference("case", this, isDefinition: true); - output.Write(' '); - if (HasNullLabel) - { - output.Write("null"); - if (!Labels.IsEmpty) - { - output.Write(", "); - output.Write(Labels.ToString()); - } - } - else + output.Write("null"); + if (!Labels.IsEmpty) { + output.Write(", "); output.Write(Labels.ToString()); } - output.Write(": "); - - body.WriteTo(output, options); } - finally + else { - output.MarkNodeEnd(this); + output.Write(Labels.ToString()); } + output.Write(": "); + + body.WriteTo(output, options); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs index f4d45c9d5e..048a178f06 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/TryInstruction.cs @@ -68,23 +68,15 @@ public override ILInstruction Clone() return clone; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(".try "); - TryBlock.WriteTo(output, options); - foreach (var handler in Handlers) - { - output.Write(' '); - handler.WriteTo(output, options); - } - } - finally + WriteILRange(output, options); + output.Write(".try "); + TryBlock.WriteTo(output, options); + foreach (var handler in Handlers) { - output.MarkNodeEnd(this); + output.Write(' '); + handler.WriteTo(output, options); } } @@ -172,29 +164,21 @@ public override InstructionFlags DirectFlags { } } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write("catch "); + if (variable != null) { - WriteILRange(output, options); - output.Write("catch "); - if (variable != null) - { - output.WriteLocalReference(variable.Name, variable, isDefinition: true); - output.Write(" : "); - Disassembler.DisassemblerHelpers.WriteOperand(output, variable.Type); - } - output.Write(" when ("); - filter.WriteTo(output, options); - output.Write(')'); - output.Write(' '); - body.WriteTo(output, options); - } - finally - { - output.MarkNodeEnd(this); + output.WriteLocalReference(variable.Name, variable, isDefinition: true); + output.Write(" : "); + Disassembler.DisassemblerHelpers.WriteOperand(output, variable.Type); } + output.Write(" when ("); + filter.WriteTo(output, options); + output.Write(')'); + output.Write(' '); + body.WriteTo(output, options); } /// @@ -233,21 +217,13 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(".try "); - TryBlock.WriteTo(output, options); - output.Write(" finally "); - finallyBlock.WriteTo(output, options); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(".try "); + TryBlock.WriteTo(output, options); + output.Write(" finally "); + finallyBlock.WriteTo(output, options); } public override StackType ResultType { @@ -338,21 +314,13 @@ 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) { - output.MarkNodeStart(this); - try - { - WriteILRange(output, options); - output.Write(".try "); - TryBlock.WriteTo(output, options); - output.Write(" fault "); - faultBlock.WriteTo(output, options); - } - finally - { - output.MarkNodeEnd(this); - } + WriteILRange(output, options); + output.Write(".try "); + TryBlock.WriteTo(output, options); + output.Write(" fault "); + faultBlock.WriteTo(output, options); } public override StackType ResultType { diff --git a/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs index 9cb0ef47d9..b4bf7bdc6b 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/UnaryInstruction.cs @@ -50,25 +50,17 @@ 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) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write(OpCode); + if (IsLifted) { - WriteILRange(output, options); - output.Write(OpCode); - if (IsLifted) - { - output.Write(".lifted"); - } - output.Write('('); - this.Argument.WriteTo(output, options); - output.Write(')'); - } - finally - { - output.MarkNodeEnd(this); + output.Write(".lifted"); } + output.Write('('); + this.Argument.WriteTo(output, options); + output.Write(')'); } } } diff --git a/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs index d0600df327..4ea93aacd2 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs @@ -40,36 +40,28 @@ partial class UsingInstruction public bool IsRefStruct { get; set; } - public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options) { - output.MarkNodeStart(this); - try + WriteILRange(output, options); + output.Write("using"); + if (IsAsync) { - WriteILRange(output, options); - output.Write("using"); - if (IsAsync) - { - output.Write(".async"); - } - if (IsRefStruct) - { - output.Write(".ref"); - } - output.Write(" ("); - Variable.WriteTo(output); - output.Write(" = "); - ResourceExpression.WriteTo(output, options); - output.WriteLine(") {"); - output.Indent(); - Body.WriteTo(output, options); - output.Unindent(); - output.WriteLine(); - output.Write("}"); + output.Write(".async"); } - finally + if (IsRefStruct) { - output.MarkNodeEnd(this); + output.Write(".ref"); } + output.Write(" ("); + Variable.WriteTo(output); + output.Write(" = "); + ResourceExpression.WriteTo(output, options); + output.WriteLine(") {"); + output.Indent(); + Body.WriteTo(output, options); + output.Unindent(); + output.WriteLine(); + output.Write("}"); } } } From c418c6ac54e230248dc2fc086078d0bd52cd93a6 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 12:06:00 +0200 Subject: [PATCH 11/21] Bridge only the debug-step marker in NodeLookup NodeLookup.AddNode indexed every annotation of every rendered node by reference identity, but the debug-step highlighter only ever looks up the DebugStepMarker; the rest were dead keys, and a shared annotation (ResolveResult and friends, copied across nodes) would resolve to whichever node rendered last. Make DebugStepMarker public and bridge only it -- behaviour-preserving for resolution while dropping the per-annotation dictionary churn on every rendered node. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../CSharp/Transforms/TransformContext.cs | 12 ++++++++--- ILSpy.Tests/Views/DebugStepsTests.cs | 12 ++++++++++- ILSpy/TextView/NodeLookup.cs | 20 ++++++++++++------- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs index 9adf66b96e..5e6ee99c60 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs @@ -144,9 +144,15 @@ static void AddCandidate(Stepper.Node step, object candidate, bool insertFirst) else step.ModifiedNodeCandidates.Add(candidate); } + } - sealed class DebugStepMarker - { - } + /// + /// 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 NodeLookup 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/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 1690169e3b..8e0dc076ce 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -32,6 +32,7 @@ using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.ILSpy; using ICSharpCode.ILSpy.AppEnv; @@ -275,7 +276,7 @@ public Task ILAst_And_TypedIL_Languages_Are_Registered_In_Debug_Builds() [AvaloniaTest] public Task NodeLookup_Resolves_Copied_Ast_Annotations() { - var marker = new object(); + var marker = new DebugStepMarker(); var original = new IdentifierExpression("old"); original.AddAnnotation(marker); var replacement = new IdentifierExpression("new").CopyAnnotationsFrom(original); @@ -287,6 +288,15 @@ public Task NodeLookup_Resolves_Copied_Ast_Annotations() "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; } diff --git a/ILSpy/TextView/NodeLookup.cs b/ILSpy/TextView/NodeLookup.cs index b68f0f4ba2..e2603bef45 100644 --- a/ILSpy/TextView/NodeLookup.cs +++ b/ILSpy/TextView/NodeLookup.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.CSharp.Transforms; namespace ICSharpCode.ILSpy.TextView { @@ -34,15 +35,20 @@ public bool TryGetRange(object node, out TextRange range) public void AddNode(object node, int start, int length) { - if (length > 0) + 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) { - nodes[node] = new TextRange(start, length); - if (node is IAnnotatable annotatable) + foreach (var annotation in annotatable.Annotations) { - foreach (var annotation in annotatable.Annotations) - { - nodes[annotation] = new TextRange(start, length); - } + if (annotation is DebugStepMarker) + nodes[annotation] = range; } } } From e1f83f6556873ca35e41ef15b1da1ab8a3fc4360 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 12:15:36 +0200 Subject: [PATCH 12/21] Record candidates for the C# step that hits the step limit When the step limit falls on a C# transform step, Stepper.Step records the node as LimitReachedStep but throws before TransformContext can attach the node's highlight candidates, so the 'show state before' view had only the bare modified node to resolve against -- and nothing if that node renders no text of its own. The IL path already records its candidates before the throw; mirror that on the C# side by attaching the candidates to the limit-reached node in the catch, then re-throwing so the pipeline still halts. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../CSharp/Transforms/TransformContext.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs index 5e6ee99c60..31dd4f263c 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs @@ -81,7 +81,22 @@ internal TransformContext(IDecompilerTypeSystem typeSystem, DecompileRun decompi [DebuggerStepThrough] internal void Step(string description, AstNode? near = null) { - TrackModifiedNode(Stepper.Step(description, modifiedNode: near), near); + Stepper.Node stepNode; + try + { + stepNode = Stepper.Step(description, modifiedNode: near); + } + catch (StepLimitReachedException) + { + // The limit fell on this step: Stepper recorded it as LimitReachedStep but threw + // before we could attach the node candidates. Attach them to the limit-reached node + // (the tree is still in its pre-mutation state) so the "show state before" view can + // locate the change, mirroring how the IL path records candidates before its throw. + if (Stepper.LimitReachedStep is { } limitStep) + TrackModifiedNode(limitStep, near); + throw; + } + TrackModifiedNode(stepNode, near); } [Conditional("STEP")] From 954ab83bfbf4b8de1adc05f0586f6c23e58931e7 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 12:15:49 +0200 Subject: [PATCH 13/21] Assert debug-step replay highlights are precise, not just present The replay tests only checked DebugStepHighlight was non-null, which the ancestor fallback satisfies unconditionally -- a regression widening every highlight to the enclosing method would have passed. Assert instead that the range lies in bounds, does not span the whole document, and (unless it is a zero-length removal caret) covers non-whitespace rendered code. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy.Tests/Views/DebugStepsTests.cs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 8e0dc076ce..7f7ddff13a 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -177,13 +177,13 @@ await Waiters.WaitForAsync( 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"); - tab.DebugStepHighlight.Should().NotBeNull("C# replay before a selected AST mutation step must locate the changed node"); + 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"); - tab.DebugStepHighlight.Should().NotBeNull("C# replay after a selected AST mutation step must locate the changed node"); + 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"); @@ -229,13 +229,13 @@ await Waiters.WaitForAsync( await tab.RestartDecompileWithStepLimit(replayStep!.BeginStep, isDebug: false, replayStep.BeginStep); tab.Text.Should().NotBeNullOrWhiteSpace("ILAst replay before a selected step must still emit IL"); - tab.DebugStepHighlight.Should().NotBeNull("ILAst replay before a selected step must locate the changed instruction"); + 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"); - tab.DebugStepHighlight.Should().NotBeNull("ILAst replay after a selected step must locate the changed instruction"); + 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. @@ -391,6 +391,26 @@ public Task DebugSteps_View_Loads_With_Filter_Applied() 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_Excludes_Leading_Indentation() { From 564cbdca13cdb547f65db8261965a63c78b85a74 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 18:32:01 +0200 Subject: [PATCH 14/21] Clear debug steps synchronously on selection to avoid a race OnSelectionChanged deferred its Steps = null via Dispatcher.Post. The selection message is raised synchronously on the UI thread right as the new selection's decompile is kicked off (AssemblyTreeModel.RaiseSelectionChanged), so a deferred clear can land on a later dispatcher cycle than the decompile's StepperUpdated populate post and wipe the freshly recorded steps -- leaving the pane empty until the next decompile (the intermittent CI timeout in the debug-step UI tests, where both C# and ILAst share this handler). Clear synchronously so the blank is pinned to the selection moment, strictly before that selection's decompile can finish and post its steps. Assisted-by: Claude:claude-opus-4-8:Claude Code --- ILSpy/ViewModels/DebugStepsPaneModel.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/ILSpy/ViewModels/DebugStepsPaneModel.cs b/ILSpy/ViewModels/DebugStepsPaneModel.cs index e264606085..1eb7abeb44 100644 --- a/ILSpy/ViewModels/DebugStepsPaneModel.cs +++ b/ILSpy/ViewModels/DebugStepsPaneModel.cs @@ -220,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) From 0ef33b8a10d51be49b6bd59bb1d505977fd83461 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 21:32:05 +0200 Subject: [PATCH 15/21] Ignore null C# debug-step produced nodes Keep existing modified-node candidates when a transform cannot provide a produced node, matching the IL transform helper and preserving highlight fallback quality. Assisted-by: OpenCode:openai/gpt-5.5:OpenCode --- ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs index 31dd4f263c..13fafaf58a 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs @@ -120,7 +120,7 @@ internal void StepEndGroup(bool keepIfEmpty = false) [Conditional("STEP")] internal void EndStep(AstNode? modifiedNode) { - if (Stepper.LastStep != null) + if (Stepper.LastStep != null && modifiedNode != null) { Stepper.LastStep.ModifiedNode = modifiedNode; TrackModifiedNode(Stepper.LastStep, modifiedNode, insertFirst: true); From 5f9d7767dd52464459c557013c1a349aa35f49f9 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 21:32:29 +0200 Subject: [PATCH 16/21] Guard deferred debug-step highlight scrolling Avoid stale dispatcher callbacks scrolling the editor after a later decompile has cleared or replaced the debug-step highlight. Assisted-by: OpenCode:openai/gpt-5.5:OpenCode --- ILSpy/TextView/DecompilerTextView.axaml.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index edbb11c0f4..4e6fc469b3 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -80,6 +80,7 @@ public partial class DecompilerTextView : UserControl BracketHighlightRenderer bracketHighlightRenderer = null!; readonly List localReferenceMarks = new(); readonly List debugStepMarks = new(); + int debugStepHighlightVersion; readonly List activeCustomGenerators = new(); RichTextColorizer? activeColorizer; FoldingManager? activeFoldingManager; @@ -1278,12 +1279,17 @@ void ApplyDebugStepHighlight(TextRange? range) // 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; - Dispatcher.UIThread.Post(() => CenterLineInView(document, line), DispatcherPriority.Background); + 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(); From 1f9db2894e24b9bedf0e8715c9baccfc38033a77 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 21:32:53 +0200 Subject: [PATCH 17/21] Disable debug-step node tracking by default Normal decompiles do not consume node ranges, so keep AvaloniaEdit node tracking opt-in and enable it only for step-limited replay output. Assisted-by: OpenCode:openai/gpt-5.5:OpenCode --- ILSpy.Tests/Views/DebugStepsTests.cs | 18 ++++++++++++++++-- ILSpy/TextView/AvaloniaEditTextOutput.cs | 10 ++++++++++ ILSpy/TextView/DecompilerTabPageModel.cs | 5 ++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/ILSpy.Tests/Views/DebugStepsTests.cs b/ILSpy.Tests/Views/DebugStepsTests.cs index 7f7ddff13a..b604870a12 100644 --- a/ILSpy.Tests/Views/DebugStepsTests.cs +++ b/ILSpy.Tests/Views/DebugStepsTests.cs @@ -411,12 +411,26 @@ static void AssertPreciseHighlight(DecompilerTabPageModel tab, string because) } } + [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(); + var output = new AvaloniaEditTextOutput { EnableNodeTracking = true }; output.Indent(); output.WriteLine(); @@ -435,7 +449,7 @@ 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(); + var output = new AvaloniaEditTextOutput { EnableNodeTracking = true }; var outer = new object(); var inner = new object(); diff --git a/ILSpy/TextView/AvaloniaEditTextOutput.cs b/ILSpy/TextView/AvaloniaEditTextOutput.cs index 7c3c25f2a6..57a321d11f 100644 --- a/ILSpy/TextView/AvaloniaEditTextOutput.cs +++ b/ILSpy/TextView/AvaloniaEditTextOutput.cs @@ -79,6 +79,12 @@ public sealed class AvaloniaEditTextOutput : ISmartTextOutput, INodeTrackingOutp /// 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; @@ -312,6 +318,8 @@ public void EndSpan() 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. @@ -321,6 +329,8 @@ public void MarkNodeStart(object node) 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/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs index 34376996e0..74be6b190b 100644 --- a/ILSpy/TextView/DecompilerTabPageModel.cs +++ b/ILSpy/TextView/DecompilerTabPageModel.cs @@ -558,7 +558,10 @@ 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( From b0ac40c99a11129a3de133ab922dc0a6b096b399 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 21:38:27 +0200 Subject: [PATCH 18/21] Handle debug-step Enter before tree items consume it TreeViewItem handles Enter for expand and collapse before bubble-phase handlers run, so install the replay shortcut in the tunnel phase to keep group-row commands reachable. Assisted-by: OpenCode:openai/gpt-5.5:OpenCode --- ILSpy/Views/DebugSteps.axaml | 1 - ILSpy/Views/DebugSteps.axaml.cs | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ILSpy/Views/DebugSteps.axaml b/ILSpy/Views/DebugSteps.axaml index 5946530330..5919970546 100644 --- a/ILSpy/Views/DebugSteps.axaml +++ b/ILSpy/Views/DebugSteps.axaml @@ -45,7 +45,6 @@ ItemsSource="{Binding Steps}" SelectedItem="{Binding SelectedStep, Mode=TwoWay}" DoubleTapped="OnTreeDoubleTapped" - KeyDown="OnTreeKeyDown" x:CompileBindings="False">