From ffd1eac3d076245dc396492fab8be08fc258ad82 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 1 Jul 2026 23:03:37 +0200 Subject: [PATCH] Share debug-step candidate recording between IL and C# The IL (Stepper) and C# (TransformContext) paths each recorded the changed node, its seam neighbours, and its ancestor chain with duplicated logic that could drift. Move the ordering/dedup/seam strategy onto Stepper.Node.RecordModifiedNode; each language keeps only its own node navigation. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../CSharp/Transforms/TransformContext.cs | 39 ++++++------- .../IL/Transforms/IILTransform.cs | 3 +- .../IL/Transforms/Stepper.cs | 57 +++++++++++++++++-- 3 files changed, 69 insertions(+), 30 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs index 13fafaf58..8dad120dc 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/TransformContext.cs @@ -18,6 +18,7 @@ #nullable enable +using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Threading; @@ -131,34 +132,26 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms { if (modifiedNode == null) return; - AddCandidate(step, modifiedNode, insertFirst); + // The marker rides CopyAnnotationsFrom so a replaced node's step still resolves to the + // emitted text; it is recorded as a second identity for the changed node. The seam + // neighbours and ancestor chain are captured from the original node on the initial pass + // (insertFirst == false); a produced-node update only prepends the new node. var marker = new DebugStepMarker(); modifiedNode.AddAnnotation(marker); - AddCandidate(step, marker, insertFirst: false); - // 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) + step.RecordModifiedNode( + modifiedNode, + insertFirst ? null : modifiedNode.NextSibling, + insertFirst ? null : modifiedNode.PrevSibling, + insertFirst ? null : Ancestors(modifiedNode), + extraIdentity: marker, + insertFirst: insertFirst); + + static IEnumerable Ancestors(AstNode node) { - 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); - } + for (var parent = node.Parent; parent != null; parent = parent.Parent) + yield return parent; } } - - 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); - } } /// diff --git a/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs index 9436e3051..f0321cff9 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs @@ -135,8 +135,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (Stepper.LastStep is { } step && modifiedNode != null) { step.ModifiedNode = modifiedNode; - if (!step.ModifiedNodeCandidates.Contains(modifiedNode)) - step.ModifiedNodeCandidates.Insert(0, modifiedNode); + step.RecordModifiedNode(modifiedNode, insertFirst: true); } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs index cc45a4690..2839dd233 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/Stepper.cs @@ -96,6 +96,50 @@ namespace ICSharpCode.Decompiler.IL.Transforms { Description = description; } + + /// + /// Records the highlight candidates for this step from an already-navigated node: the + /// node's identity (optionally a second identity such as a debug-step marker), its + /// immediate siblings as seam anchors, and its ancestor chain. Callers pass the + /// neighbours because and the C# AST expose navigation + /// differently; the ordering/dedup/seam strategy lives here so the two languages' + /// recording can't drift. marks a produced-node update + /// (from EndStep): the node is preferred over existing candidates and the seam and + /// ancestor anchors are left untouched, since they were captured from the original node. + /// + public void RecordModifiedNode( + object node, + object? nextSibling = null, + object? previousSibling = null, + IEnumerable? ancestors = null, + object? extraIdentity = null, + bool insertFirst = false) + { + AddCandidate(node, insertFirst); + if (extraIdentity != null) + AddCandidate(extraIdentity, insertFirst: false); + if (insertFirst) + return; + if (nextSibling != null) + SeamAnchors.Add((nextSibling, false)); + if (previousSibling != null) + SeamAnchors.Add((previousSibling, true)); + if (ancestors != null) + { + foreach (var ancestor in ancestors) + AncestorCandidates.Add(ancestor); + } + } + + void AddCandidate(object candidate, bool insertFirst) + { + if (ModifiedNodeCandidates.Contains(candidate)) + return; + if (insertFirst) + ModifiedNodeCandidates.Insert(0, candidate); + else + ModifiedNodeCandidates.Add(candidate); + } } readonly Stack groups; @@ -137,18 +181,21 @@ namespace ICSharpCode.Decompiler.IL.Transforms // surviving ancestor (ultimately the ILFunction) still resolves. if (near != null) { - stepNode.ModifiedNodeCandidates.Add(near); + object? next = null, prev = null; if (near.Parent is { } parent) { int index = near.ChildIndex; if (index + 1 < parent.Children.Count) - stepNode.SeamAnchors.Add((parent.Children[index + 1], false)); + next = parent.Children[index + 1]; if (index - 1 >= 0) - stepNode.SeamAnchors.Add((parent.Children[index - 1], true)); + prev = parent.Children[index - 1]; } - for (var node = near.Parent; node != null; node = node.Parent) + stepNode.RecordModifiedNode(near, next, prev, Ancestors(near)); + + static IEnumerable Ancestors(ILInstruction inst) { - stepNode.AncestorCandidates.Add(node); + for (var node = inst.Parent; node != null; node = node.Parent) + yield return node; } } if (step == StepLimit)