dotnet / interactive

.NET Interactive combines the power of .NET with many other languages to create notebooks, REPLs, and embedded coding experiences. Share code, explore data, write, and learn across your apps in ways you couldn't before.
MIT License
2.88k stars 382 forks source link

PlainTextFormatter<T> causes infinite loop when T is IEnumerable<T> #3625

Open duyang76 opened 1 month ago

duyang76 commented 1 month ago

I can replicate this issue with a bare minimum example: a class implementing IEnumerable of itself with an empty child list.

Version: 1.0.522904

I cannot post from my company's internal network, so I'm typing from my phone and taking a screenshot below. But it is pretty obvious.

IMG20240805111604

duyang76 commented 1 month ago

Can someone take a look or provide some insight?

The class definition is below. It runs fine. But if I create an instance in polyglot notebook, the kernel crashes due to stack overflow. There is no infinite-loop in the code, just recursive call on the child nodes (which is empty). It seems that the .net interactive or polyglot causes infinite-loop somewhere.

public class TestIEnumerator : IEnumerable<TestIEnumerator>
{
    private List<TestIEnumerator> children; 
    public List<TestIEnumerator> Children
    {
        get
        {
            if (children==null) children = new List<TestIEnumerator>();
            return children;
        }
    }

    public IEnumerator<TestIEnumerator> GetEnumerator()
    {
        List<TestIEnumerator> childList = new List<TestIEnumerator>();
        recurseChildren(this, ref childList, 0);
        return childList.GetEnumerator();
    }

    private void recurseChildren(TestIEnumerator f, ref List<TestIEnumerator>coll, int lvl)
    {
        coll.Add(f);
        Console.WriteLine("RecurseChildren level: " + lvl);
        foreach(TestIEnumerator tmp in f.Children)
        {
            RecurseChildren(tmp, ref coll, lvl+1);
        }
    }

    System.Collection.IEnumerator System.Collection.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}
duyang76 commented 2 weeks ago

Can someone provide some thoughts?

duyang76 commented 4 days ago

I looked at the stack trace and source code, and found that the infinite loop is caused by the CreateForAnyEnumerable() in PlainTextFormatter{T}.cs (https://github.com/dotnet/interactive/blob/main/src/Microsoft.DotNet.Interactive.Formatting/PlainTextFormatter%7BT%7D.cs#L73). In particular, the IEnumerable<> look through around line 84. Even though FormatContext has limit for recursion depth, it does not apply here. So, it causes infinite loop if if T is IEnumerable\<T>.

I am able to work around by overriding PlainTextFormatter\<T>.Default to my own formatter. But what is the correct fix? Is the look-through still needed if T is IEnumerable\<T>?