mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
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 Code
121 lines
5.3 KiB
C#
121 lines
5.3 KiB
C#
// 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" }));
|
|
}
|
|
}
|
|
}
|