Browse Source

Flip AST storage from the linked list to slots

Children were kept in a per-node doubly-linked list with the slot accessors
layered over it as a view. Storage now is the slot model: each node stores its
children in generated backing fields, AstNodeCollection<T> is backed by a
List<T>, and the flattened child-index space is owned by generated
GetChildCount/GetChild/SetChild/GetChildSlot members, with sibling navigation,
the role API and Clone re-expressed over them and indices renumbered lazily. A
DEBUG CheckInvariant runs after each transform, the analog of the IL
pipeline's per-transform check, so a transform that corrupts the tree fails at
that transform. Output is unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3807/head
Siegfried Pammer 3 weeks ago committed by Siegfried Pammer
parent
commit
94678e9c69
  1. 140
      ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs
  2. 10
      ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
  3. 7
      ICSharpCode.Decompiler/CSharp/StatementBuilder.cs
  4. 634
      ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs
  5. 221
      ICSharpCode.Decompiler/CSharp/Syntax/AstNodeCollection.cs
  6. 14
      ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/Accessor.cs
  7. 9
      ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/EventDeclaration.cs
  8. 5
      ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs

140
ICSharpCode.Decompiler.Generators/DecompilerSyntaxTreeGenerator.cs

@ -30,7 +30,7 @@ namespace ICSharpCode.Decompiler.Generators;
[Generator] [Generator]
internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
{ {
record AstNodeAdditions(string NodeName, bool NeedsAcceptImpls, bool NeedsVisitor, bool NeedsNullNode, bool NeedsPatternPlaceholder, int NullNodeBaseCtorParamCount, bool IsTypeNode, string VisitMethodName, string VisitMethodParamType, EquatableArray<(string Member, string TypeName, bool RecursiveMatch, bool MatchAny)>? MembersToMatch, EquatableArray<(string RoleExpr, bool IsCollection, string PropertyName, string PropertyType, bool IsOverride)>? Slots); record AstNodeAdditions(string NodeName, bool NeedsAcceptImpls, bool NeedsVisitor, bool NeedsNullNode, bool NeedsPatternPlaceholder, int NullNodeBaseCtorParamCount, bool IsTypeNode, string VisitMethodName, string VisitMethodParamType, EquatableArray<(string Member, string TypeName, bool RecursiveMatch, bool MatchAny)>? MembersToMatch, EquatableArray<(string RoleExpr, bool IsCollection, string PropertyName, string PropertyType, string ElementType, bool IsOverride, bool IsNullable)>? Slots);
AstNodeAdditions GetAstNodeAdditions(GeneratorAttributeSyntaxContext context, CancellationToken ct) AstNodeAdditions GetAstNodeAdditions(GeneratorAttributeSyntaxContext context, CancellationToken ct)
{ {
@ -78,7 +78,7 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
// Collect the slot schema: child properties tagged [Slot], in declaration order. The // Collect the slot schema: child properties tagged [Slot], in declaration order. The
// attribute names the Role expression to use; single vs collection comes from the type. // attribute names the Role expression to use; single vs collection comes from the type.
List<(string RoleExpr, bool IsCollection, string PropertyName, string PropertyType, bool IsOverride)>? slots = null; List<(string RoleExpr, bool IsCollection, string PropertyName, string PropertyType, string ElementType, bool IsOverride, bool IsNullable)>? slots = null;
foreach (var m in targetSymbol.GetMembers()) foreach (var m in targetSymbol.GetMembers())
{ {
if (m is not IPropertySymbol property) if (m is not IPropertySymbol property)
@ -89,8 +89,14 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
slots ??= new(); slots ??= new();
string roleExpr = (string)slotAttr.ConstructorArguments[0].Value!; string roleExpr = (string)slotAttr.ConstructorArguments[0].Value!;
bool isCollection = property.Type.MetadataName == "AstNodeCollection`1"; bool isCollection = property.Type.MetadataName == "AstNodeCollection`1";
string propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); bool isNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated;
slots.Add((roleExpr, isCollection, property.Name, propertyType, property.IsOverride)); var unannotated = property.Type.WithNullableAnnotation(NullableAnnotation.NotAnnotated);
string propertyType = unannotated.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
// For a collection slot, the element type is the single type argument of AstNodeCollection<T>.
string elementType = isCollection
? ((INamedTypeSymbol)property.Type).TypeArguments[0].ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)
: propertyType;
slots.Add((roleExpr, isCollection, property.Name, propertyType, elementType, property.IsOverride, isNullable));
} }
return new(targetSymbol.Name, !targetSymbol.MemberNames.Contains("AcceptVisitor"), return new(targetSymbol.Name, !targetSymbol.MemberNames.Contains("AcceptVisitor"),
@ -102,10 +108,16 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
visitMethodName, paramTypeName, membersToMatch?.ToEquatableArray(), slots?.ToEquatableArray()); visitMethodName, paramTypeName, membersToMatch?.ToEquatableArray(), slots?.ToEquatableArray());
} }
// Backing-field name for a slot property. Prefixed to avoid colliding with hand-written fields
// in the same partial class; the generated file carries an auto-generated header, so naming-style
// rules do not apply.
static string FieldName(string propertyName) => "slot_" + propertyName;
void WriteGeneratedMembers(SourceProductionContext context, AstNodeAdditions source) void WriteGeneratedMembers(SourceProductionContext context, AstNodeAdditions source)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.AppendLine("// <auto-generated/>");
builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;"); builder.AppendLine("namespace ICSharpCode.Decompiler.CSharp.Syntax;");
builder.AppendLine(); builder.AppendLine();
@ -277,37 +289,107 @@ internal class DecompilerSyntaxTreeGenerator : IIncrementalGenerator
{ {
var slots = slotsArray.ToList(); var slots = slotsArray.ToList();
// Implementing half of each [Slot] partial property: stored over the linked list for now. // Backing fields and the partial-property bodies. A single slot stores a nullable field and
// A slot re-declared from an inherited contract member (Part I.3 flatten) is an override. // substitutes the role's null object when empty (pre-NRT); a collection slot owns a lazily
foreach (var (roleExpr, isCollection, name, type, isOverride) in slots) // created AstNodeCollection bound to this node. A slot re-declared from an inherited contract
// member (Part I.3 flatten) is an override.
foreach (var (roleExpr, isCollection, name, type, elementType, isOverride, isNullable) in slots)
{ {
builder.AppendLine($"\tpublic {(isOverride ? "override " : "")}partial {type} {name}"); string field = FieldName(name);
builder.AppendLine("\t{"); if (isCollection)
builder.AppendLine($"\t\tget {{ return GetChild{(isCollection ? "ren" : "")}ByRole({roleExpr}); }}"); {
if (!isCollection) builder.AppendLine($"\tAstNodeCollection<{elementType}>? {field};");
builder.AppendLine($"\t\tset {{ SetChildByRole({roleExpr}, value); }}"); builder.AppendLine($"\tpublic {(isOverride ? "override " : "")}partial AstNodeCollection<{elementType}> {name} => {field} ??= new AstNodeCollection<{elementType}>(this, {roleExpr});");
builder.AppendLine("\t}"); }
else
{
builder.AppendLine($"\t{type}? {field};");
builder.AppendLine($"\tpublic {(isOverride ? "override " : "")}partial {type} {name}");
builder.AppendLine("\t{");
builder.AppendLine($"\t\tget => {field}{(isNullable ? "" : $" ?? {roleExpr}.NullObject")};");
builder.AppendLine($"\t\tset => SetChildNode(ref {field}, value, {roleExpr});");
builder.AppendLine("\t}");
}
} }
// Slot schema (transitional bridge): source-ordered slots mapped to roles. // Flattened child-index space: slots in declaration order, a single slot occupying one index
builder.AppendLine($"\tinternal override int SlotCount => {slots.Count};"); // (even when empty), a collection slot a contiguous run of its current length.
builder.AppendLine("\tinternal override Role GetSlotRole(int slotIndex)"); builder.Append("\tinternal override int GetChildCount() => ");
builder.Append(string.Join(" + ", slots.Select(s => s.IsCollection ? $"({FieldName(s.PropertyName)}?.Count ?? 0)" : "1")));
builder.AppendLine(";");
builder.AppendLine("\tinternal override AstNode? GetChild(int index)");
builder.AppendLine("\t{"); builder.AppendLine("\t{");
builder.AppendLine("\t\tswitch (slotIndex)"); builder.AppendLine("\t\tint i = index;");
builder.AppendLine("\t\t{"); foreach (var s in slots)
for (int i = 0; i < slots.Count; i++) {
builder.AppendLine($"\t\t\tcase {i}: return {slots[i].RoleExpr};"); if (s.IsCollection)
builder.AppendLine("\t\t\tdefault: throw new System.ArgumentOutOfRangeException(nameof(slotIndex));"); {
builder.AppendLine("\t\t}"); string field = FieldName(s.PropertyName);
builder.AppendLine($"\t\t{{ int n = {field}?.Count ?? 0; if (i < n) return {field}![i]; i -= n; }}");
}
else
{
builder.AppendLine($"\t\tif (i == 0) return {FieldName(s.PropertyName)}; i -= 1;");
}
}
builder.AppendLine("\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));");
builder.AppendLine("\t}"); builder.AppendLine("\t}");
builder.AppendLine("\tinternal override bool IsCollectionSlot(int slotIndex)");
builder.AppendLine("\tinternal override void SetChild(int index, AstNode? value)");
builder.AppendLine("\t{"); builder.AppendLine("\t{");
builder.AppendLine("\t\tswitch (slotIndex)"); builder.AppendLine("\t\tint i = index;");
builder.AppendLine("\t\t{"); foreach (var s in slots)
for (int i = 0; i < slots.Count; i++) {
builder.AppendLine($"\t\t\tcase {i}: return {(slots[i].IsCollection ? "true" : "false")};"); if (s.IsCollection)
builder.AppendLine("\t\t\tdefault: throw new System.ArgumentOutOfRangeException(nameof(slotIndex));"); {
builder.AppendLine("\t\t}"); string field = FieldName(s.PropertyName);
builder.AppendLine($"\t\t{{ int n = {field}?.Count ?? 0; if (i < n) {{ {field}![i] = ({s.ElementType})value!; return; }} i -= n; }}");
}
else
{
builder.AppendLine($"\t\tif (i == 0) {{ SetChildNode(ref {FieldName(s.PropertyName)}, ({s.PropertyType}?)value, {s.RoleExpr}); return; }} i -= 1;");
}
}
builder.AppendLine("\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));");
builder.AppendLine("\t}");
builder.AppendLine("\tinternal override Role GetChildSlot(int index)");
builder.AppendLine("\t{");
builder.AppendLine("\t\tint i = index;");
foreach (var s in slots)
{
if (s.IsCollection)
builder.AppendLine($"\t\t{{ int n = {FieldName(s.PropertyName)}?.Count ?? 0; if (i < n) return {s.RoleExpr}; i -= n; }}");
else
builder.AppendLine($"\t\tif (i == 0) return {s.RoleExpr}; i -= 1;");
}
builder.AppendLine("\t\tthrow new System.ArgumentOutOfRangeException(nameof(index));");
builder.AppendLine("\t}");
if (slots.Any(s => s.IsCollection))
{
builder.AppendLine("\tinternal override AstNodeCollection? GetCollectionByRole(Role role)");
builder.AppendLine("\t{");
foreach (var s in slots.Where(s => s.IsCollection))
builder.AppendLine($"\t\tif (role == {s.RoleExpr}) return {s.PropertyName};");
builder.AppendLine("\t\treturn base.GetCollectionByRole(role);");
builder.AppendLine("\t}");
}
builder.AppendLine("\tinternal override void CloneChildrenInto(AstNode copyNode)");
builder.AppendLine("\t{");
builder.AppendLine($"\t\tvar copy = ({source.NodeName})copyNode;");
foreach (var s in slots)
builder.AppendLine($"\t\tcopy.{FieldName(s.PropertyName)} = null;");
foreach (var s in slots)
{
string field = FieldName(s.PropertyName);
if (s.IsCollection)
builder.AppendLine($"\t\tif ({field} != null) foreach (var c in {field}) copy.{s.PropertyName}.Add(({s.ElementType})c.Clone());");
else
builder.AppendLine($"\t\tif ({field} != null) copy.{s.PropertyName} = ({s.PropertyType}){field}.Clone();");
}
builder.AppendLine("\t}"); builder.AppendLine("\t}");
} }

10
ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs

@ -716,6 +716,9 @@ namespace ICSharpCode.Decompiler.CSharp
{ {
CancellationToken.ThrowIfCancellationRequested(); CancellationToken.ThrowIfCancellationRequested();
transform.Run(rootNode, context); 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(); CancellationToken.ThrowIfCancellationRequested();
rootNode.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); rootNode.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true });
@ -1433,9 +1436,10 @@ namespace ICSharpCode.Decompiler.CSharp
// Constraints are not copied because explicit interface implementations cannot have constraints. CS0460 // Constraints are not copied because explicit interface implementations cannot have constraints. CS0460
methodDecl.Body = new BlockStatement(); methodDecl.Body = new BlockStatement();
methodDecl.Body.AddChild(new Comment( var commentStatement = new EmptyStatement();
"ILSpy generated this explicit interface implementation from .override directive in " + memberDecl.Name), commentStatement.AddTrailingTrivia(new Comment(
Roles.Comment); "ILSpy generated this explicit interface implementation from .override directive in " + memberDecl.Name));
methodDecl.Body.Add(commentStatement);
var forwardingCall = new InvocationExpression(new MemberReferenceExpression(new ThisReferenceExpression(), memberDecl.Name, var forwardingCall = new InvocationExpression(new MemberReferenceExpression(new ThisReferenceExpression(), memberDecl.Name,
methodDecl.TypeParameters.Select(tp => new SimpleType(tp.Name))), methodDecl.TypeParameters.Select(tp => new SimpleType(tp.Name))),
methodDecl.Parameters.Select(ForwardParameter) methodDecl.Parameters.Select(ForwardParameter)

7
ICSharpCode.Decompiler/CSharp/StatementBuilder.cs

@ -1361,9 +1361,10 @@ namespace ICSharpCode.Decompiler.CSharp
methodDecl.Parameters.Add(new ParameterDeclaration { ParameterModifier = ReferenceKind.In, Type = new SimpleType("T"), Name = "temp" }); methodDecl.Parameters.Add(new ParameterDeclaration { ParameterModifier = ReferenceKind.In, Type = new SimpleType("T"), Name = "temp" });
methodDecl.Body = new BlockStatement(); methodDecl.Body = new BlockStatement();
methodDecl.Body.AddChild(new Comment( var commentStatement = new EmptyStatement();
"ILSpy generated this function to help ensure overload resolution can pick the overload using 'in'"), commentStatement.AddTrailingTrivia(new Comment(
Roles.Comment); "ILSpy generated this function to help ensure overload resolution can pick the overload using 'in'"));
methodDecl.Body.Add(commentStatement);
methodDecl.Body.Add(new ReturnStatement(new DirectionExpression(FieldDirection.Ref, new IdentifierExpression("temp")))); methodDecl.Body.Add(new ReturnStatement(new DirectionExpression(FieldDirection.Ref, new IdentifierExpression("temp"))));
blockStatement.Statements.Add( blockStatement.Statements.Add(

634
ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs

@ -44,10 +44,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
internal static readonly Role<AstNode?> RootRole = new Role<AstNode?>("Root", null); internal static readonly Role<AstNode?> RootRole = new Role<AstNode?>("Root", null);
AstNode? parent; AstNode? parent;
AstNode? prevSibling; // Flattened index of this node within its parent's child-index space (-1 when unparented).
AstNode? nextSibling; // Recomputed lazily by the parent (EnsureChildIndices) after a structural mutation.
AstNode? firstChild; internal int childIndex = -1;
AstNode? lastChild;
// Flags, from least significant to most significant bits: // Flags, from least significant to most significant bits:
// - Role.RoleIndexBits: role index // - Role.RoleIndexBits: role index
@ -192,36 +191,76 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
} }
public AstNode? NextSibling { public AstNode? NextSibling {
get { return nextSibling; } get {
if (parent == null)
return null;
parent.EnsureChildIndices();
int count = parent.GetChildCount();
for (int i = childIndex + 1; i < count; i++)
{
AstNode? c = parent.GetChild(i);
if (c != null)
return c;
}
return null;
}
} }
public AstNode? PrevSibling { public AstNode? PrevSibling {
get { return prevSibling; } get {
if (parent == null)
return null;
parent.EnsureChildIndices();
for (int i = childIndex - 1; i >= 0; i--)
{
AstNode? c = parent.GetChild(i);
if (c != null)
return c;
}
return null;
}
} }
public AstNode? FirstChild { public AstNode? FirstChild {
get { return firstChild; } get {
int count = GetChildCount();
for (int i = 0; i < count; i++)
{
AstNode? c = GetChild(i);
if (c != null)
return c;
}
return null;
}
} }
public AstNode? LastChild { public AstNode? LastChild {
get { return lastChild; } get {
for (int i = GetChildCount() - 1; i >= 0; i--)
{
AstNode? c = GetChild(i);
if (c != null)
return c;
}
return null;
}
} }
public bool HasChildren { public bool HasChildren {
get { get {
return firstChild != null; return FirstChild != null;
} }
} }
public IEnumerable<AstNode> Children { public IEnumerable<AstNode> Children {
get { get {
AstNode? next; AstNode? next;
for (AstNode? cur = firstChild; cur != null; cur = next) for (AstNode? cur = FirstChild; cur != null; cur = next)
{ {
Debug.Assert(cur.parent == this); Debug.Assert(cur.parent == this);
// Remember next before yielding cur. // Remember next before yielding cur.
// This allows removing/replacing nodes while iterating through the list. // This allows removing/replacing nodes while iterating.
next = cur.nextSibling; next = cur.NextSibling;
yield return cur; yield return cur;
} }
} }
@ -286,16 +325,18 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
Stack<AstNode?> nextStack = new Stack<AstNode?>(); Stack<AstNode?> nextStack = new Stack<AstNode?>();
nextStack.Push(null); nextStack.Push(null);
AstNode? pos = firstChild; AstNode? pos = FirstChild;
while (pos != null) while (pos != null)
{ {
// Remember next before yielding pos. // Remember next before yielding pos.
// This allows removing/replacing nodes while iterating through the list. // This allows removing/replacing nodes while iterating.
if (pos.nextSibling != null) AstNode? posNext = pos.NextSibling;
nextStack.Push(pos.nextSibling); if (posNext != null)
nextStack.Push(posNext);
yield return pos; yield return pos;
if (pos.firstChild != null && (descendIntoChildren == null || descendIntoChildren(pos))) AstNode? posFirstChild = pos.FirstChild;
pos = pos.firstChild; if (posFirstChild != null && (descendIntoChildren == null || descendIntoChildren(pos)))
pos = posFirstChild;
else else
pos = nextStack.Pop(); pos = nextStack.Pop();
} }
@ -309,11 +350,14 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
{ {
if (role == null) if (role == null)
throw new ArgumentNullException(nameof(role)); throw new ArgumentNullException(nameof(role));
uint roleIndex = role.Index; int count = GetChildCount();
for (var cur = firstChild; cur != null; cur = cur.nextSibling) for (int i = 0; i < count; i++)
{ {
if ((cur.flags & roleIndexMask) == roleIndex) if (GetChildSlot(i) == role)
return (T)cur; {
AstNode? c = GetChild(i);
return c != null ? (T)c : role.NullObject;
}
} }
return role.NullObject; return role.NullObject;
} }
@ -330,81 +374,134 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
public AstNodeCollection<T> GetChildrenByRole<T>(Role<T> role) where T : AstNode public AstNodeCollection<T> GetChildrenByRole<T>(Role<T> role) where T : AstNode
{ {
return new AstNodeCollection<T>(this, role); if (role == null)
throw new ArgumentNullException(nameof(role));
AstNodeCollection? collection = GetCollectionByRole(role);
// A node has no children of a role it does not declare a collection slot for. Reads of such
// a role (e.g. the parameters of a non-indexer property) get a detached empty collection;
// writes go through AddChild/SetChildByRole, which reject a missing role.
return collection != null ? (AstNodeCollection<T>)collection : new AstNodeCollection<T>(this, role);
} }
protected void SetChildByRole<T>(Role<T> role, T newChild) where T : AstNode protected void SetChildByRole<T>(Role<T> role, T newChild) where T : AstNode
{ {
AstNode oldChild = GetChildByRole(role); SetChildByRoleUntyped(role, newChild);
if (oldChild.IsNull)
AddChild(newChild, role);
else
oldChild.ReplaceWith(newChild);
} }
#region Slot bridge #region Slot storage contract
// Transitional slot-accessor layer implemented OVER the linked-list child model. The // Each concrete node's slots form a flattened child-index space, in source-declaration order.
// eventual slot model assigns each node type a fixed, source-ordered set of slots; here we // A single slot occupies exactly one index (even when empty, where GetChild returns null); a
// expose that schema while storage is still the linked list. A "single slot" maps to one // collection slot occupies a contiguous run of its current length. The generator emits these
// Role (at most one child of that role); a "collection slot" maps to a Role that may hold // four members per node from its [Slot] partial-property declarations; nodes without children
// many children. GetChild returns the canonical child for a single slot, or the first child // (and the null/placeholder nodes) keep the zero-child defaults below.
// of a collection slot.
// internal virtual int GetChildCount() => 0;
// Converted nodes override SlotCount/GetSlotRole/IsCollectionSlot. The default implementation
// reports zero slots, so unconverted nodes keep working unchanged and the slot-order internal virtual AstNode? GetChild(int index) => throw new ArgumentOutOfRangeException(nameof(index));
// invariant skips them.
internal virtual void SetChild(int index, AstNode? value) => throw new ArgumentOutOfRangeException(nameof(index));
internal virtual Role GetChildSlot(int index) => throw new ArgumentOutOfRangeException(nameof(index));
internal virtual int SlotCount => 0; // Returns the collection occupying the slot with the given role, or null if there is none.
// Overridden by the generator for nodes that have collection slots.
internal virtual AstNodeCollection? GetCollectionByRole(Role role) => null;
internal virtual Role GetSlotRole(int slotIndex) // Deep-copies this node's children into the (memberwise-cloned) copy, which initially shares
// this node's child references. Overridden by the generator for nodes that have slots.
internal virtual void CloneChildrenInto(AstNode copy) { }
// Whether this node's children currently carry correct flattened childIndex values. Cleared on
// every structural mutation and restored lazily on the first index read, so bulk construction
// (many appends with no interleaved index reads) renumbers once instead of once per mutation.
bool childIndicesValid = true;
// Marks this node's children's flattened indices stale. Called by the slot setters and the
// collection after any structural change.
internal void InvalidateChildIndices()
{ {
throw new ArgumentOutOfRangeException(nameof(slotIndex)); childIndicesValid = false;
} }
internal virtual bool IsCollectionSlot(int slotIndex) // Assigns each child its flattened index if a mutation has invalidated them. The flattened-index
// arithmetic lives in the generated GetChild/GetChildCount, which depend only on the backing
// fields, so this is well-defined regardless of the current childIndex values.
void EnsureChildIndices()
{ {
throw new ArgumentOutOfRangeException(nameof(slotIndex)); if (childIndicesValid)
return;
childIndicesValid = true;
int count = GetChildCount();
for (int i = 0; i < count; i++)
{
AstNode? c = GetChild(i);
if (c != null)
c.childIndex = i;
}
} }
/// <summary> /// <summary>
/// Gets the child occupying the single slot at <paramref name="slotIndex"/>. /// Recursively verifies the slot structure of this subtree (DEBUG only): every child's Parent
/// For collection slots this returns the first child of the slot's role. /// points back here, its stored flattened index matches its slot position, and its runtime type
/// is valid in the slot's role. The transform pipeline runs this after each transform, the
/// analog of <c>ILInstruction.CheckInvariant</c>, so a transform that corrupts the tree fails at
/// the exact transform rather than as a downstream output diff.
/// </summary> /// </summary>
internal AstNode GetChild(int slotIndex) [System.Diagnostics.Conditional("DEBUG")]
internal void CheckInvariant()
{ {
Role role = GetSlotRole(slotIndex); EnsureChildIndices();
for (var cur = firstChild; cur != null; cur = cur.nextSibling) int count = GetChildCount();
for (int i = 0; i < count; i++)
{ {
if ((cur.flags & roleIndexMask) == role.Index) AstNode? child = GetChild(i);
return cur; if (child == null)
continue; // empty optional single slot
Debug.Assert(child.parent == this, "child's Parent must point back to this node");
Debug.Assert(child.childIndex == i, "child's flattened index must match its slot position");
Debug.Assert(GetChildSlot(i).IsValid(child), "child's type must be valid in its slot's role");
child.CheckInvariant();
} }
// Every role used for a single slot has a null object, so an AST property never returns null.
return (AstNode)role.NullObjectUntyped!;
} }
internal void SetChild(int slotIndex, AstNode? newChild) // Writes a single-slot backing field: detaches the old child, attaches the new one, renumbers.
// A null or null-object value empties the slot. Called by generated single-slot property setters.
internal void SetChildNode<T>(ref T? field, T? value, Role role) where T : AstNode
{ {
if (IsCollectionSlot(slotIndex)) T? newValue = (value == null || value.IsNull) ? null : value;
throw new InvalidOperationException("SetChild is not valid for collection slots; mutate the collection instead."); if (field == newValue)
Role role = GetSlotRole(slotIndex); return;
AstNode oldChild = GetChild(slotIndex); if (newValue != null)
if (oldChild.IsNull)
{
if (newChild != null && !newChild.IsNull)
AddChildUnsafe(newChild, role);
}
else
{ {
oldChild.ReplaceWith(newChild); if (newValue == this)
throw new ArgumentException("Cannot add a node to itself as a child.", nameof(value));
if (newValue.parent != null)
{
// Allow lifting a node out of the subtree being replaced, e.g.
// "assignment.Right = ((BinaryOperatorExpression)assignment.Right).Right;".
if (field != null && newValue.Ancestors.Contains(field))
newValue.Remove();
else
throw new ArgumentException("Node is already used in another tree.", nameof(value));
}
} }
field?.ClearParentAndIndex();
field = newValue;
newValue?.SetParentAndRole(this, role);
InvalidateChildIndices();
} }
// Note: document-order == slot-order is a POST-FLIP invariant, not a bridge invariant. internal void SetParentAndRole(AstNode newParent, Role role)
// During the transitional bridge the linked list still carries positional token children {
// (e.g. the Roles.Comma trailing-comma marker) and comments whose document position encodes parent = newParent;
// output meaning, so it legitimately diverges from slot order. The invariant is re-introduced SetRole(role);
// once storage flips to slots (Phase 3c), where it holds by construction; until then the }
// Pretty suite gates output. See ROLES_FREE_SLOT_AST_DESIGN.md (R8 / Phase 3c).
internal void ClearParentAndIndex()
{
parent = null;
childIndex = -1;
}
#endregion #endregion
public void AddChild<T>(T child, Role<T> role) where T : AstNode public void AddChild<T>(T child, Role<T> role) where T : AstNode
@ -413,10 +510,6 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
throw new ArgumentNullException(nameof(role)); throw new ArgumentNullException(nameof(role));
if (child == null || child.IsNull) if (child == null || child.IsNull)
return; return;
if (child == this)
throw new ArgumentException("Cannot add a node to itself as a child.", nameof(child));
if (child.parent != null)
throw new ArgumentException("Node is already used in another tree.", nameof(child));
AddChildUnsafe(child, role); AddChildUnsafe(child, role);
} }
@ -424,76 +517,72 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
{ {
if (child == null || child.IsNull) if (child == null || child.IsNull)
return; return;
if (child == this)
throw new ArgumentException("Cannot add a node to itself as a child.", nameof(child));
if (child.parent != null)
throw new ArgumentException("Node is already used in another tree.", nameof(child));
AddChildUnsafe(child, child.Role); AddChildUnsafe(child, child.Role);
} }
/// <summary> /// <summary>
/// Adds a child without performing any safety checks. /// Adds a child into the slot matching <paramref name="role"/> (appending to a collection slot,
/// or filling a single slot).
/// </summary> /// </summary>
internal void AddChildUnsafe(AstNode child, Role role) internal void AddChildUnsafe(AstNode child, Role role)
{ {
child.parent = this; AstNodeCollection? collection = GetCollectionByRole(role);
child.SetRole(role); if (collection != null)
if (firstChild == null) collection.AddNode(child);
{
lastChild = firstChild = child;
}
else else
SetChildByRoleUntyped(role, child);
}
// Sets the single slot matching the role (used by the non-generic mutation API). Symmetric with
// GetChildByRole, which returns the null object for a role this node does not declare a slot for:
// a node has no child of a role it has no slot for, so writing one is a no-op.
internal void SetChildByRoleUntyped(Role role, AstNode? child)
{
int count = GetChildCount();
for (int i = 0; i < count; i++)
{ {
lastChild!.nextSibling = child; if (GetChildSlot(i) == role)
child.prevSibling = lastChild; {
lastChild = child; SetChild(i, child == null || child.IsNull ? null : child);
return;
}
} }
throw new InvalidOperationException($"{GetType().Name} has no slot for role '{role}'.");
} }
public void InsertChildBefore<T>(AstNode? nextSibling, T child, Role<T> role) where T : AstNode public void InsertChildBefore<T>(AstNode? nextSibling, T child, Role<T> role) where T : AstNode
{ {
if (role == null) if (role == null)
throw new ArgumentNullException(nameof(role)); throw new ArgumentNullException(nameof(role));
if (nextSibling == null || nextSibling.IsNull)
{
AddChild(child, role);
return;
}
if (child == null || child.IsNull) if (child == null || child.IsNull)
return; return;
if (child.parent != null) AstNodeCollection? collection = GetCollectionByRole(role);
throw new ArgumentException("Node is already used in another tree.", nameof(child)); if (collection != null)
if (nextSibling.parent != this) collection.InsertNodeBefore((nextSibling == null || nextSibling.IsNull) ? null : nextSibling, child);
throw new ArgumentException("NextSibling is not a child of this node.", nameof(nextSibling)); else
// No need to test for "Cannot add children to null nodes", SetChildByRoleUntyped(role, child);
// as there isn't any valid nextSibling in null nodes.
InsertChildBeforeUnsafe(nextSibling, child, role);
} }
internal void InsertChildBeforeUnsafe(AstNode nextSibling, AstNode child, Role role) internal void InsertChildBeforeUnsafe(AstNode nextSibling, AstNode child, Role role)
{ {
child.parent = this; AstNodeCollection? collection = GetCollectionByRole(role);
child.SetRole(role); if (collection != null)
child.nextSibling = nextSibling; collection.InsertNodeBefore(nextSibling, child);
child.prevSibling = nextSibling.prevSibling;
if (nextSibling.prevSibling != null)
{
Debug.Assert(nextSibling.prevSibling.nextSibling == nextSibling);
nextSibling.prevSibling.nextSibling = child;
}
else else
{ SetChildByRoleUntyped(role, child);
Debug.Assert(firstChild == nextSibling);
firstChild = child;
}
nextSibling.prevSibling = child;
} }
public void InsertChildAfter<T>(AstNode? prevSibling, T child, Role<T> role) where T : AstNode public void InsertChildAfter<T>(AstNode? prevSibling, T child, Role<T> role) where T : AstNode
{ {
InsertChildBefore((prevSibling == null || prevSibling.IsNull) ? firstChild : prevSibling.nextSibling, child, role); if (role == null)
throw new ArgumentNullException(nameof(role));
if (child == null || child.IsNull)
return;
AstNodeCollection? collection = GetCollectionByRole(role);
if (collection != null)
collection.InsertNodeAfter((prevSibling == null || prevSibling.IsNull) ? null : prevSibling, child);
else
SetChildByRoleUntyped(role, child);
} }
/// <summary> /// <summary>
@ -501,32 +590,15 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
/// </summary> /// </summary>
public void Remove() public void Remove()
{ {
if (parent != null) if (parent == null)
{ return;
if (prevSibling != null) parent.EnsureChildIndices();
{ Role role = parent.GetChildSlot(childIndex);
Debug.Assert(prevSibling.nextSibling == this); AstNodeCollection? collection = parent.GetCollectionByRole(role);
prevSibling.nextSibling = nextSibling; if (collection != null)
} collection.RemoveNode(this);
else else
{ parent.SetChild(childIndex, null);
Debug.Assert(parent.firstChild == this);
parent.firstChild = nextSibling;
}
if (nextSibling != null)
{
Debug.Assert(nextSibling.prevSibling == this);
nextSibling.prevSibling = prevSibling;
}
else
{
Debug.Assert(parent.lastChild == this);
parent.lastChild = prevSibling;
}
parent = null;
prevSibling = null;
nextSibling = null;
}
} }
/// <summary> /// <summary>
@ -545,11 +617,13 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
{ {
throw new InvalidOperationException(this.IsNull ? "Cannot replace the null nodes" : "Cannot replace the root node"); throw new InvalidOperationException(this.IsNull ? "Cannot replace the null nodes" : "Cannot replace the root node");
} }
parent.EnsureChildIndices();
Role role = parent.GetChildSlot(childIndex);
// Because this method doesn't statically check the new node's type with the role, // Because this method doesn't statically check the new node's type with the role,
// we perform a runtime test: // we perform a runtime test:
if (!this.Role.IsValid(newNode)) if (!role.IsValid(newNode))
{ {
throw new ArgumentException(string.Format("The new node '{0}' is not valid in the role {1}", newNode.GetType().Name, this.Role.ToString()), nameof(newNode)); throw new ArgumentException(string.Format("The new node '{0}' is not valid in the role {1}", newNode.GetType().Name, role.ToString()), nameof(newNode));
} }
if (newNode.parent != null) if (newNode.parent != null)
{ {
@ -565,34 +639,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
throw new ArgumentException("Node is already used in another tree.", nameof(newNode)); throw new ArgumentException("Node is already used in another tree.", nameof(newNode));
} }
} }
newNode.parent = parent; parent.SetChild(childIndex, newNode);
newNode.SetRole(this.Role);
newNode.prevSibling = prevSibling;
newNode.nextSibling = nextSibling;
if (prevSibling != null)
{
Debug.Assert(prevSibling.nextSibling == this);
prevSibling.nextSibling = newNode;
}
else
{
Debug.Assert(parent.firstChild == this);
parent.firstChild = newNode;
}
if (nextSibling != null)
{
Debug.Assert(nextSibling.prevSibling == this);
nextSibling.prevSibling = newNode;
}
else
{
Debug.Assert(parent.lastChild == this);
parent.lastChild = newNode;
}
parent = null;
prevSibling = null;
nextSibling = null;
} }
public AstNode? ReplaceWith(Func<AstNode, AstNode?> replaceFunction) public AstNode? ReplaceWith(Func<AstNode, AstNode?> replaceFunction)
@ -604,7 +651,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
throw new InvalidOperationException(this.IsNull ? "Cannot replace the null nodes" : "Cannot replace the root node"); throw new InvalidOperationException(this.IsNull ? "Cannot replace the null nodes" : "Cannot replace the root node");
} }
AstNode oldParent = parent; AstNode oldParent = parent;
AstNode? oldSuccessor = nextSibling; AstNode? oldSuccessor = NextSibling;
Role oldRole = this.Role; Role oldRole = this.Role;
Remove(); Remove();
AstNode? replacement = replaceFunction(this); AstNode? replacement = replaceFunction(this);
@ -634,22 +681,13 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
public AstNode Clone() public AstNode Clone()
{ {
AstNode copy = (AstNode)MemberwiseClone(); AstNode copy = (AstNode)MemberwiseClone();
// First, reset the shallow pointer copies
copy.parent = null; copy.parent = null;
copy.firstChild = null; copy.childIndex = -1;
copy.lastChild = null; // Deep-copy the children (CloneChildrenInto first drops the shallow field copies that
copy.prevSibling = null; // MemberwiseClone left pointing at this node's children).
copy.nextSibling = null; CloneChildrenInto(copy);
// Then perform a deep copy:
for (AstNode? cur = firstChild; cur != null; cur = cur.nextSibling)
{
copy.AddChildUnsafe(cur.Clone(), cur.Role);
}
// Finally, clone the annotation, if necessary // Finally, clone the annotation, if necessary
copy.CloneAnnotations(); copy.CloneAnnotations();
return copy; return copy;
} }
@ -686,11 +724,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
} }
PatternMatching.INode? PatternMatching.INode.NextSibling { PatternMatching.INode? PatternMatching.INode.NextSibling {
get { return nextSibling; } get { return NextSibling; }
} }
PatternMatching.INode? PatternMatching.INode.FirstChild { PatternMatching.INode? PatternMatching.INode.FirstChild {
get { return firstChild; } get { return FirstChild; }
} }
#endregion #endregion
@ -777,222 +815,6 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
return prev; return prev;
} }
#region GetNodeAt
/// <summary>
/// Gets the node specified by T at the location line, column. This is useful for getting a specific node from the tree. For example searching
/// the current method declaration.
/// (End exclusive)
/// </summary>
public AstNode? GetNodeAt(int line, int column, Predicate<AstNode>? pred = null)
{
return GetNodeAt(new TextLocation(line, column), pred);
}
/// <summary>
/// Gets the node specified by pred at location. This is useful for getting a specific node from the tree. For example searching
/// the current method declaration.
/// (End exclusive)
/// </summary>
public AstNode? GetNodeAt(TextLocation location, Predicate<AstNode>? pred = null)
{
AstNode? result = null;
AstNode node = this;
while (node.LastChild != null)
{
var child = node.LastChild;
while (child != null && child.StartLocation > location)
child = child.prevSibling;
if (child != null && location < child.EndLocation)
{
if (pred == null || pred(child))
result = child;
node = child;
}
else
{
// found no better child node - therefore the parent is the right one.
break;
}
}
return result;
}
/// <summary>
/// Gets the node specified by T at the location line, column. This is useful for getting a specific node from the tree. For example searching
/// the current method declaration.
/// (End exclusive)
/// </summary>
public T? GetNodeAt<T>(int line, int column) where T : AstNode
{
return GetNodeAt<T>(new TextLocation(line, column));
}
/// <summary>
/// Gets the node specified by T at location. This is useful for getting a specific node from the tree. For example searching
/// the current method declaration.
/// (End exclusive)
/// </summary>
public T? GetNodeAt<T>(TextLocation location) where T : AstNode
{
T? result = null;
AstNode node = this;
while (node.LastChild != null)
{
var child = node.LastChild;
while (child != null && child.StartLocation > location)
child = child.prevSibling;
if (child != null && location < child.EndLocation)
{
if (child is T)
result = (T)child;
node = child;
}
else
{
// found no better child node - therefore the parent is the right one.
break;
}
}
return result;
}
#endregion
#region GetAdjacentNodeAt
/// <summary>
/// Gets the node specified by pred at the location line, column. This is useful for getting a specific node from the tree. For example searching
/// the current method declaration.
/// (End inclusive)
/// </summary>
public AstNode? GetAdjacentNodeAt(int line, int column, Predicate<AstNode>? pred = null)
{
return GetAdjacentNodeAt(new TextLocation(line, column), pred);
}
/// <summary>
/// Gets the node specified by pred at location. This is useful for getting a specific node from the tree. For example searching
/// the current method declaration.
/// (End inclusive)
/// </summary>
public AstNode? GetAdjacentNodeAt(TextLocation location, Predicate<AstNode>? pred = null)
{
AstNode? result = null;
AstNode node = this;
while (node.LastChild != null)
{
var child = node.LastChild;
while (child != null && child.StartLocation > location)
child = child.prevSibling;
if (child != null && location <= child.EndLocation)
{
if (pred == null || pred(child))
result = child;
node = child;
}
else
{
// found no better child node - therefore the parent is the right one.
break;
}
}
return result;
}
/// <summary>
/// Gets the node specified by T at the location line, column. This is useful for getting a specific node from the tree. For example searching
/// the current method declaration.
/// (End inclusive)
/// </summary>
public T? GetAdjacentNodeAt<T>(int line, int column) where T : AstNode
{
return GetAdjacentNodeAt<T>(new TextLocation(line, column));
}
/// <summary>
/// Gets the node specified by T at location. This is useful for getting a specific node from the tree. For example searching
/// the current method declaration.
/// (End inclusive)
/// </summary>
public T? GetAdjacentNodeAt<T>(TextLocation location) where T : AstNode
{
T? result = null;
AstNode node = this;
while (node.LastChild != null)
{
var child = node.LastChild;
while (child != null && child.StartLocation > location)
child = child.prevSibling;
if (child != null && location <= child.EndLocation)
{
if (child is T t)
result = t;
node = child;
}
else
{
// found no better child node - therefore the parent is the right one.
break;
}
}
return result;
}
#endregion
/// <summary>
/// Gets the node that fully contains the range from startLocation to endLocation.
/// </summary>
public AstNode GetNodeContaining(TextLocation startLocation, TextLocation endLocation)
{
for (AstNode? child = firstChild; child != null; child = child.nextSibling)
{
if (child.StartLocation <= startLocation && endLocation <= child.EndLocation)
return child.GetNodeContaining(startLocation, endLocation);
}
return this;
}
/// <summary>
/// Returns the root nodes of all subtrees that are fully contained in the specified region.
/// </summary>
public IEnumerable<AstNode> GetNodesBetween(int startLine, int startColumn, int endLine, int endColumn)
{
return GetNodesBetween(new TextLocation(startLine, startColumn), new TextLocation(endLine, endColumn));
}
/// <summary>
/// Returns the root nodes of all subtrees that are fully contained between <paramref name="start"/> and <paramref name="end"/> (inclusive).
/// </summary>
public IEnumerable<AstNode> GetNodesBetween(TextLocation start, TextLocation end)
{
AstNode? node = this;
while (node != null)
{
AstNode? next;
if (start <= node.StartLocation && node.EndLocation <= end)
{
// Remember next before yielding node.
// This allows iteration to continue when the caller removes/replaces the node.
next = node.GetNextNode();
yield return node;
}
else
{
if (node.EndLocation <= start)
{
next = node.GetNextNode();
}
else
{
next = node.FirstChild;
}
}
if (next != null && next.StartLocation > end)
yield break;
node = next;
}
}
/// <summary> /// <summary>
/// Gets the node as formatted C# output. /// Gets the node as formatted C# output.
/// </summary> /// </summary>

221
ICSharpCode.Decompiler/CSharp/Syntax/AstNodeCollection.cs

@ -1,14 +1,14 @@
// Copyright (c) 2010-2013 AlphaSierraPapa for the SharpDevelop Team // Copyright (c) 2010-2013 AlphaSierraPapa for the SharpDevelop Team
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy of this // 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 // software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge, // 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 // 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: // 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 // The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software. // substantial portions of the Software.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // 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 // 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 // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
@ -16,51 +16,82 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
#nullable enable
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching; using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;
namespace ICSharpCode.Decompiler.CSharp.Syntax namespace ICSharpCode.Decompiler.CSharp.Syntax
{ {
/// <summary> /// <summary>
/// Represents the children of an AstNode that have a specific role. /// Non-generic base of <see cref="AstNodeCollection{T}"/>, letting the <see cref="AstNode"/> base
/// manipulate a collection slot (remove an element by reference) without knowing its element type.
/// </summary> /// </summary>
public class AstNodeCollection<T> : ICollection<T>, IReadOnlyCollection<T> public abstract class AstNodeCollection
{
internal abstract void AddNode(AstNode node);
internal abstract void InsertNodeBefore(AstNode? existing, AstNode node);
internal abstract void InsertNodeAfter(AstNode? existing, AstNode node);
internal abstract bool RemoveNode(AstNode node);
}
/// <summary>
/// Represents the children of an <see cref="AstNode"/> that occupy one collection slot.
/// The elements are stored in a list owned by the collection; the parent's flattened child-index
/// space contains them as a contiguous run, renumbered lazily by the parent after a mutation.
/// </summary>
public class AstNodeCollection<T> : AstNodeCollection, ICollection<T>, IReadOnlyList<T>
where T : AstNode where T : AstNode
{ {
readonly AstNode node; readonly AstNode parent;
readonly Role<T> role; readonly Role<T> role;
readonly List<T> list = new List<T>();
public AstNodeCollection(AstNode node, Role<T> role) public AstNodeCollection(AstNode parent, Role<T> role)
{ {
if (node == null) this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
throw new ArgumentNullException(nameof(node)); this.role = role ?? throw new ArgumentNullException(nameof(role));
if (role == null)
throw new ArgumentNullException(nameof(role));
this.node = node;
this.role = role;
} }
public int Count { public int Count {
get { get { return list.Count; }
int count = 0; }
uint roleIndex = role.Index;
for (AstNode cur = node.FirstChild; cur != null; cur = cur.NextSibling) public T this[int index] {
{ get { return list[index]; }
if (cur.RoleIndex == roleIndex) set {
count++; T old = list[index];
} if (old == value)
return count; return;
ValidateNewChild(value);
old.ClearParentAndIndex();
list[index] = value;
value.SetParentAndRole(parent, role);
parent.InvalidateChildIndices();
} }
} }
void ValidateNewChild(T child)
{
if (child == null)
throw new ArgumentNullException(nameof(child));
if (child == parent)
throw new ArgumentException("Cannot add a node to itself as a child.", nameof(child));
if (child.Parent != null)
throw new ArgumentException("Node is already used in another tree.", nameof(child));
}
public void Add(T element) public void Add(T element)
{ {
node.AddChild(element, role); if (element == null || element.IsNull)
return;
ValidateNewChild(element);
list.Add(element);
element.SetParentAndRole(parent, role);
parent.InvalidateChildIndices();
} }
public void AddRange(IEnumerable<T> nodes) public void AddRange(IEnumerable<T> nodes)
@ -69,7 +100,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
// Example: collection.AddRange(collection); // Example: collection.AddRange(collection);
if (nodes != null) if (nodes != null)
{ {
foreach (T node in nodes.ToList()) foreach (T node in nodes is ICollection<T> ? nodes : new List<T>(nodes))
Add(node); Add(node);
} }
} }
@ -88,12 +119,11 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
{ {
// Evaluate 'nodes' first, since it might change when we call Clear() // Evaluate 'nodes' first, since it might change when we call Clear()
// Example: collection.ReplaceWith(collection); // Example: collection.ReplaceWith(collection);
if (nodes != null) List<T>? newNodes = nodes != null ? new List<T>(nodes) : null;
nodes = nodes.ToList();
Clear(); Clear();
if (nodes != null) if (newNodes != null)
{ {
foreach (T node in nodes) foreach (T node in newNodes)
Add(node); Add(node);
} }
} }
@ -102,7 +132,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
{ {
if (targetCollection == null) if (targetCollection == null)
throw new ArgumentNullException(nameof(targetCollection)); throw new ArgumentNullException(nameof(targetCollection));
foreach (T node in this) foreach (T node in list.ToArray())
{ {
node.Remove(); node.Remove();
targetCollection.Add(node); targetCollection.Add(node);
@ -111,47 +141,82 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
public bool Contains(T element) public bool Contains(T element)
{ {
return element != null && element.Parent == node && element.RoleIndex == role.Index; return element != null && element.Parent == parent && element.RoleIndex == role.Index;
}
public int IndexOf(T element)
{
if (element == null || element.Parent != parent)
return -1;
return list.IndexOf(element);
} }
public bool Remove(T element) public bool Remove(T element)
{ {
if (Contains(element)) int index = IndexOf(element);
{ if (index < 0)
element.Remove();
return true;
}
else
{
return false; return false;
} list.RemoveAt(index);
element.ClearParentAndIndex();
parent.InvalidateChildIndices();
return true;
}
internal override void AddNode(AstNode node)
{
Add((T)node);
}
internal override void InsertNodeBefore(AstNode? existing, AstNode node)
{
// 'existing' may belong to a different slot (e.g. the node after the last collection
// element); in that case it is not found here and the new node is appended.
if (existing is T e && IndexOf(e) >= 0)
InsertBefore(e, (T)node);
else
Add((T)node);
}
internal override void InsertNodeAfter(AstNode? existing, AstNode node)
{
if (existing is T e && IndexOf(e) >= 0)
InsertAfter(e, (T)node);
else
Insert(0, (T)node);
}
internal override bool RemoveNode(AstNode node)
{
return node is T typed && Remove(typed);
} }
public void CopyTo(T[] array, int arrayIndex) public void CopyTo(T[] array, int arrayIndex)
{ {
foreach (T item in this) list.CopyTo(array, arrayIndex);
array[arrayIndex++] = item;
} }
public void Clear() public void Clear()
{ {
foreach (T item in this) foreach (T item in list)
item.Remove(); item.ClearParentAndIndex();
list.Clear();
parent.InvalidateChildIndices();
} }
public IEnumerable<T> Detach() public IEnumerable<T> Detach()
{ {
foreach (T item in this) T[] items = list.ToArray();
yield return item.Detach(); Clear();
return items;
} }
/// <summary> /// <summary>
/// Returns the first element for which the predicate returns true, /// Returns the first element for which the predicate returns true,
/// or the null node (AstNode with IsNull=true) if no such object is found. /// or the null node (AstNode with IsNull=true) if no such object is found.
/// </summary> /// </summary>
public T FirstOrNullObject(Func<T, bool> predicate = null) public T FirstOrNullObject(Func<T, bool>? predicate = null)
{ {
foreach (T item in this) foreach (T item in list)
if (predicate == null || predicate(item)) if (predicate == null || predicate(item))
return item; return item;
return role.NullObject; return role.NullObject;
@ -161,10 +226,10 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
/// Returns the last element for which the predicate returns true, /// Returns the last element for which the predicate returns true,
/// or the null node (AstNode with IsNull=true) if no such object is found. /// or the null node (AstNode with IsNull=true) if no such object is found.
/// </summary> /// </summary>
public T LastOrNullObject(Func<T, bool> predicate = null) public T LastOrNullObject(Func<T, bool>? predicate = null)
{ {
T result = role.NullObject; T result = role.NullObject;
foreach (T item in this) foreach (T item in list)
if (predicate == null || predicate(item)) if (predicate == null || predicate(item))
result = item; result = item;
return result; return result;
@ -174,18 +239,17 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
get { return false; } get { return false; }
} }
// Tolerates removing or replacing the current element during enumeration (the common
// transform pattern), matching the previous linked-list collection's behavior.
public IEnumerator<T> GetEnumerator() public IEnumerator<T> GetEnumerator()
{ {
uint roleIndex = role.Index; int pos = 0;
AstNode next; while (pos < list.Count)
for (AstNode cur = node.FirstChild; cur != null; cur = next)
{ {
Debug.Assert(cur.Parent == node); T cur = list[pos];
// Remember next before yielding cur. yield return cur;
// This allows removing/replacing nodes while iterating through the list. if (pos < list.Count && ReferenceEquals(list[pos], cur))
next = cur.NextSibling; pos++;
if (cur.RoleIndex == roleIndex)
yield return (T)cur;
} }
} }
@ -197,31 +261,39 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
#region Equals and GetHashCode implementation #region Equals and GetHashCode implementation
public override int GetHashCode() public override int GetHashCode()
{ {
return node.GetHashCode() ^ role.GetHashCode(); return parent.GetHashCode() ^ role.GetHashCode();
} }
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
AstNodeCollection<T> other = obj as AstNodeCollection<T>; return obj is AstNodeCollection<T> other && this.parent == other.parent && this.role == other.role;
if (other == null)
return false;
return this.node == other.node && this.role == other.role;
} }
#endregion #endregion
internal bool DoMatch(AstNodeCollection<T> other, Match match) internal bool DoMatch(AstNodeCollection<T> other, Match match)
{ {
return Pattern.DoMatchCollection(role, node.FirstChild, other.node.FirstChild, match); return Pattern.DoMatchCollection(role, parent.FirstChild, other.parent.FirstChild, match);
} }
public void InsertAfter(T existingItem, T newItem) public void InsertAfter(T existingItem, T newItem)
{ {
node.InsertChildAfter(existingItem, newItem, role); Insert(IndexOf(existingItem) + 1, newItem);
} }
public void InsertBefore(T existingItem, T newItem) public void InsertBefore(T existingItem, T newItem)
{ {
node.InsertChildBefore(existingItem, newItem, role); int index = IndexOf(existingItem);
Insert(index < 0 ? list.Count : index, newItem);
}
void Insert(int index, T newItem)
{
if (newItem == null || newItem.IsNull)
return;
ValidateNewChild(newItem);
list.Insert(index, newItem);
newItem.SetParentAndRole(parent, role);
parent.InvalidateChildIndices();
} }
/// <summary> /// <summary>
@ -229,17 +301,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
/// </summary> /// </summary>
public void AcceptVisitor(IAstVisitor visitor) public void AcceptVisitor(IAstVisitor visitor)
{ {
uint roleIndex = role.Index; foreach (T item in this)
AstNode next; item.AcceptVisitor(visitor);
for (AstNode cur = node.FirstChild; cur != null; cur = next)
{
Debug.Assert(cur.Parent == node);
// Remember next before yielding cur.
// This allows removing/replacing nodes while iterating through the list.
next = cur.NextSibling;
if (cur.RoleIndex == roleIndex)
cur.AcceptVisitor(visitor);
}
} }
} }
} }

14
ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/Accessor.cs

@ -56,6 +56,20 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
public AccessorKind Kind { get; set; } public AccessorKind Kind { get; set; }
// An accessor is printed as its keyword (get/set/init/add/remove), never an identifier, so it
// carries no name. The contract members are overridden to no-ops: shared decompiler code sets a
// name on every method-like entity (e.g. explicit interface implementations), which is irrelevant
// here and must not throw.
public override string Name {
get { return string.Empty; }
set { }
}
public override Identifier NameToken {
get { return Identifier.Null; }
set { }
}
[Slot("AttributeRole")] [Slot("AttributeRole")]
public override partial AstNodeCollection<AttributeSection> Attributes { get; } public override partial AstNodeCollection<AttributeSection> Attributes { get; }

9
ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/EventDeclaration.cs

@ -85,6 +85,12 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
get { return SymbolKind.Event; } get { return SymbolKind.Event; }
} }
[Slot("AttributeRole")]
public override partial AstNodeCollection<AttributeSection> Attributes { get; }
[Slot("Roles.Type")]
public override partial AstType ReturnType { get; set; }
/// <summary> /// <summary>
/// Gets/Sets the type reference of the interface that is explicitly implemented. /// Gets/Sets the type reference of the interface that is explicitly implemented.
/// Null node if this member is not an explicit interface implementation. /// Null node if this member is not an explicit interface implementation.
@ -92,6 +98,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
[Slot("PrivateImplementationTypeRole")] [Slot("PrivateImplementationTypeRole")]
public partial AstType PrivateImplementationType { get; set; } public partial AstType PrivateImplementationType { get; set; }
[Slot("Roles.Identifier")]
public override partial Identifier NameToken { get; set; }
[Slot("AddAccessorRole")] [Slot("AddAccessorRole")]
public partial Accessor AddAccessor { get; set; } public partial Accessor AddAccessor { get; set; }

5
ICSharpCode.Decompiler/CSharp/Transforms/IntroduceExtensionMethods.cs

@ -116,10 +116,9 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
} }
if (invocationExpression.Target is IdentifierExpression identifierExpression) if (invocationExpression.Target is IdentifierExpression identifierExpression)
{ {
identifierExpression.Detach();
memberRefExpr = new MemberReferenceExpression(firstArgument.Detach(), method.Name, identifierExpression.TypeArguments.Detach()); memberRefExpr = new MemberReferenceExpression(firstArgument.Detach(), method.Name, identifierExpression.TypeArguments.Detach());
// Replace in place so the target keeps its slot position; assigning after a invocationExpression.Target = memberRefExpr;
// Detach would re-append it behind the arguments in document order.
identifierExpression.ReplaceWith(memberRefExpr);
} }
else else
{ {

Loading…
Cancel
Save