mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
The generated GetChildNodes materialized a List<AstNode> (plus a boxed List.Enumerator at every foreach) for each node, so AstNode.Children and the visitor's per-node child walk allocated three objects per traversal. Decompiling System.Private.CoreLib that came to ~1.7 GB of extra garbage, roughly +7% over the linked-list model the slot tree replaced. A yield iterator removes the List but trades it for an equally costly per-node state machine, so it is not enough on its own. Enumerate children through a by-value struct enumerator over the existing FirstChild/NextSibling primitives, capturing each child's successor before it is yielded so a transform may still remove or replace the current child mid-traversal. AstNodeCollection<T> gets the same struct treatment for a direct foreach. Child enumeration now allocates nothing, bringing total allocations back to the linked-list baseline at byte-identical output (full Pretty suite green with CheckInvariant active). Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3807/head
6 changed files with 309 additions and 46 deletions
@ -0,0 +1,120 @@ |
|||||||
|
// 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 System.Linq; |
||||||
|
|
||||||
|
using ICSharpCode.Decompiler.CSharp.Syntax; |
||||||
|
|
||||||
|
using NUnit.Framework; |
||||||
|
|
||||||
|
namespace ICSharpCode.Decompiler.Tests.Syntax |
||||||
|
{ |
||||||
|
/// <summary>
|
||||||
|
/// Pins the mutation-during-enumeration contract of <see cref="AstNodeCollection{T}.Enumerator"/>:
|
||||||
|
/// the cursor follows node identity hand-over-hand (like the old linked-list enumerator), so a transform
|
||||||
|
/// may remove or replace the current element mid-foreach, and inserting or removing other elements only
|
||||||
|
/// shifts the backing-list indices without skipping or re-yielding a surviving element. Each case also
|
||||||
|
/// exercises the enumerator's DEBUG position guard, which asserts when the cursor mislocates.
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture] |
||||||
|
public class AstNodeCollectionEnumeratorTests |
||||||
|
{ |
||||||
|
static ExpressionStatement Stmt(string name) => new ExpressionStatement(new IdentifierExpression(name)); |
||||||
|
|
||||||
|
static string NameOf(Statement statement) => ((IdentifierExpression)((ExpressionStatement)statement).Expression).Identifier; |
||||||
|
|
||||||
|
// Walks the collection, invoking 'mutate' once when the named statement is the current element, and
|
||||||
|
// returns the names yielded in order. The foreach uses the struct enumerator under test.
|
||||||
|
static List<string> EnumerateWith(BlockStatement block, string mutateAt, System.Action<BlockStatement, Statement> mutate) |
||||||
|
{ |
||||||
|
var visited = new List<string>(); |
||||||
|
foreach (Statement statement in block.Statements) |
||||||
|
{ |
||||||
|
visited.Add(NameOf(statement)); |
||||||
|
if (mutate != null && NameOf(statement) == mutateAt) |
||||||
|
mutate(block, statement); |
||||||
|
} |
||||||
|
return visited; |
||||||
|
} |
||||||
|
|
||||||
|
static BlockStatement Block(params string[] names) |
||||||
|
{ |
||||||
|
var block = new BlockStatement(); |
||||||
|
foreach (string name in names) |
||||||
|
block.Statements.Add(Stmt(name)); |
||||||
|
return block; |
||||||
|
} |
||||||
|
|
||||||
|
[Test] |
||||||
|
public void PlainEnumeration_YieldsAllInOrder() |
||||||
|
{ |
||||||
|
var block = Block("a", "b", "c", "d"); |
||||||
|
Assert.That(EnumerateWith(block, null, null), Is.EqualTo(new[] { "a", "b", "c", "d" })); |
||||||
|
} |
||||||
|
|
||||||
|
[Test] |
||||||
|
public void RemovingCurrent_DoesNotSkipTheSuccessor() |
||||||
|
{ |
||||||
|
var block = Block("a", "b", "c", "d"); |
||||||
|
var visited = EnumerateWith(block, "a", (b, current) => current.Remove()); |
||||||
|
Assert.That(visited, Is.EqualTo(new[] { "a", "b", "c", "d" })); |
||||||
|
Assert.That(block.Statements.Select(NameOf), Is.EqualTo(new[] { "b", "c", "d" })); |
||||||
|
} |
||||||
|
|
||||||
|
[Test] |
||||||
|
public void ReplacingCurrent_AdvancesPastTheReplacement() |
||||||
|
{ |
||||||
|
var block = Block("a", "b", "c", "d"); |
||||||
|
var visited = EnumerateWith(block, "b", (b, current) => current.ReplaceWith(Stmt("b2"))); |
||||||
|
// The replacement takes the current slot but is not re-visited.
|
||||||
|
Assert.That(visited, Is.EqualTo(new[] { "a", "b", "c", "d" })); |
||||||
|
Assert.That(block.Statements.Select(NameOf), Is.EqualTo(new[] { "a", "b2", "c", "d" })); |
||||||
|
} |
||||||
|
|
||||||
|
[Test] |
||||||
|
public void RemovingAnAlreadyVisitedElement_DoesNotDisturbTheRemainder() |
||||||
|
{ |
||||||
|
var block = Block("a", "b", "c", "d"); |
||||||
|
// Removing 'a' while 'c' is current shifts 'd' down one index; the identity resume absorbs it.
|
||||||
|
var visited = EnumerateWith(block, "c", (b, current) => b.Statements.First(s => NameOf(s) == "a").Remove()); |
||||||
|
Assert.That(visited, Is.EqualTo(new[] { "a", "b", "c", "d" })); |
||||||
|
} |
||||||
|
|
||||||
|
[Test] |
||||||
|
public void InsertingBeforeTheCursor_DoesNotSkipSurvivingElements() |
||||||
|
{ |
||||||
|
var block = Block("a", "b", "c", "d"); |
||||||
|
// Inserting 'x' before the current 'b' shifts every later index up one; the cursor still resumes
|
||||||
|
// on its captured successor 'c'. The freshly inserted node is not visited (it is behind the cursor).
|
||||||
|
var visited = EnumerateWith(block, "b", (b, current) => b.Statements.InsertBefore((Statement)current, Stmt("x"))); |
||||||
|
Assert.That(visited, Is.EqualTo(new[] { "a", "b", "c", "d" })); |
||||||
|
Assert.That(block.Statements.Select(NameOf), Is.EqualTo(new[] { "a", "x", "b", "c", "d" })); |
||||||
|
} |
||||||
|
|
||||||
|
[Test] |
||||||
|
public void RemovingTheCapturedSuccessor_StopsEnumeration() |
||||||
|
{ |
||||||
|
var block = Block("a", "b", "c", "d"); |
||||||
|
// 'c' is the successor captured when 'b' is yielded; removing it leaves the cursor with no node to
|
||||||
|
// resume on, so enumeration stops -- the one mutation the identity walk cannot follow.
|
||||||
|
var visited = EnumerateWith(block, "b", (b, current) => b.Statements.First(s => NameOf(s) == "c").Remove()); |
||||||
|
Assert.That(visited, Is.EqualTo(new[] { "a", "b" })); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue