waf / CSharpRepl

A command line C# REPL with syntax highlighting – explore the language, libraries and nuget packages interactively.
https://fuqua.io/CSharpRepl/
Mozilla Public License 2.0
2.95k stars 112 forks source link

Problem with Span<T> output #318

Closed kindermannhubert closed 8 months ago

kindermannhubert commented 1 year ago

Version

last

What happened?

"hello".AsSpan()

image

Emik03 commented 1 year ago

Is it possible to intercept the abstract syntax tree and append a call to .ToString()/.ToArray()? That's the only way I can see this being fixed since Span<T> is by-design a stack-only type, and bypassing this would probably lead to undefined behavior.

The only problem with that is that not all ref-structs are so easily convertible. Take the following type for example.

ref struct A;

You cannot call .ToString() on it because that comes from ValueType, A cannot be passed into generic methods either. You would need to scaffold an entire function inside of the script context, and inject that function, which I think is a bit overkill.

Looking at other REPLs, they don't handle this at all.

Mono REPL:

csharp> Span<int>.Empty 
(1,12): error CS0029: Cannot implicitly convert type `System.Span<int>' to `object'
kindermannhubert commented 8 months ago

Yeah, that's a non-trivial problem. At first, I thought the problem is our formatting (I didn't read the callstack carefully) but it is a limitation of Roslyn's scripting engine.

I have a working prototype of suggested input interception. I believe it should suffice.

For Span<T> / ReadonlySpan<T> I will slice it to get the first 1000 (?) items and call ToArray() (if the span is >1000 we should write an error, that the result will be truncated). For other ref structs I would output just some error that we cannot do it.

image

kindermannhubert commented 8 months ago

Hm, maybe the interception shouldn't return T[] but some custom type:

class __Span<T>
{
    public readonly T[] Array;
    public readonly OriginalLength;
    //custom ToString
}

So the output would be more clear and instead of char[5] (from the example) there would be something like ReadOnlySpan<char>(Length: 5).

Emik03 commented 8 months ago

If possible though, do add the edgecase that ReadOnlySpan<char>/Span<char> has over other types. Specifically .ToString() should call new string(char[]) if typeof(T) == typeof(char), and keep the array otherwise.

yg-i commented 6 months ago

I wonder if this issue is related to the problem I'm having. Basically, when I run this:

ReadOnlySpan<byte> test = "test"u8;

I get

(1,1): error CS8345: Field or auto-implemented property cannot be of type 'ReadOnlySpan<byte>' unless it is an instance member of a ref struct.

But saving it to a file and dotnet run causes no errors.

Emik03 commented 6 months ago

In dotnet run, that's a top-level statement.

[CompilerGenerated]
internal class Program
{
    private unsafe static void <Main>$(string[] args)
    {
        ReadOnlySpan<byte> readOnlySpan = "test"u8;
    }
}

In a REPL context, you're inside a class, and any variables you declare are actually part of its fields.

public sealed class Submission#0
{
    public ReadOnlySpan<byte> test;

    [SpecialName]
    public async global::System.Threading.Tasks.Task<object> <Initialize>()
    {
        test = "test"u8;
        return null;
    }

    public Submission#0(object[] submissionArray)
    {
        submissionArray[1] = this;
    }

    [SpecialName]
    public static global::System.Threading.Tasks.Task<object> <Factory>(object[] submissionArray)
    {
        return new Submission#0(submissionArray).<Initialize>();
    }
}

ReadOnlySpan<T> is a ref struct: It must live on the stack at all times. A class by design is always heap-allocated, so therefore you cannot put a span as a field, and this error message makes sense. You can however work around it by wrapping it:

new Action(() =>
{
    ReadOnlySpan<byte> test = "test"u8;
})()

Hope this helps!

yg-i commented 6 months ago

This is super helpful - many thanks!