6

Why does the following code not compile in C# 11?

// Example 1 - fails
class C {
    public Span<int> M(ref int arg) {
        Span<int> span;
        span = new Span<int>(ref arg);
        return span;
    }
}

It produces two compile errors:

error CS9077: Cannot return a parameter by reference 'arg' through a ref parameter; it can only be returned in a return statement.

error CS8347: Cannot use a result of 'Span.Span(ref int)' in this context because it may expose variables referenced by parameter 'reference' outside of their declaration scope.

Neither of them makes sense to me: my code doesn't try to return arg by a ref parameter, and it can't expose variables referenced by arg outside of their declaration scope.

By comparison, the following two pieces of code compile successfully:

// Example 2 - succeeds
class C {
    public Span<int> M(ref int arg) {
        Span<int> span = new Span<int>(ref arg);
        return span;
    }
}
// Example 3 - succeeds
class C {
    public Span<int> M(Span<int> arg) {
        Span<int> span;
        span = new Span<int>(ref arg[0]);
        return span;
    }
}

My intuition is that Span<int> internally holds a ref field of type int, so the escape rules should work the same for Examples 1 and 3 above (which, apparently, they do not).

I made an analogous experiment with a ref struct explicitly holding a ref field:

ref struct S {
    public ref int X;
}

Now, the following code fails to compile:

// Example 4 - fails
class C {
    public S M(ref int arg) {
        S instance;
        instance.X = ref arg;
        return instance;
    }
}

It produces the following error, which at least makes slightly more sense to me:

error CS9079: Cannot ref-assign 'arg' to 'X' because 'arg' can only escape the current method through a return statement.

By comparison, the following two pieces of code compile successfully (with the definition of S above):

// Example 5 - succeeds
class C {
    public S M(ref int arg) {
        S instance = new S() { X = ref arg };
        return instance;
    }
}
// Example 6 - succeeds
class C {
    public S M(S arg) {
        S instance;
        instance.X = ref arg.X;
        return instance;
    }
}

In particular, if arg can only escape the current method through a return statement, as in the error message for Example 4 above, while doesn't the same hold for arg.X in Example 6?

I tried to find the answer in the documentation for low level struct improvements, but I failed. Moreover, that documentation page seems to contradict itself in several places.

Bartosz
  • 461
  • 2
  • 9
  • This issue has been reported to the Roslyn Github and closed as "By Design" https://github.com/dotnet/roslyn/issues/53014 – NineBerry Dec 25 '22 at 18:12
  • OK, this explains why Examples 1 vs 2 (or 4 vs 5) may behave differently. But how about Examples 1 vs 3 (or 4 vs 6)? For instance, it seems that the "lifetime" of `arg` in Example 4 is tighter than the "lifetime" of `arg.X` in Example 6, which I don't understand. – Bartosz Dec 25 '22 at 18:21
  • I think there is probably a bug in one specific place in the Roslyn compiler where one specific decision is made that misses the `ref` modifier on the parameter and therefore treats the parameter like a local variable instead of a returnable reference. – NineBerry Dec 25 '22 at 18:52

2 Answers2

0

are you sure you are using C# 11? Using linqpad with .Net 7 your "fails to compile" example worked fine for me:

Compiles Fine

Update: doesn't compile if using the daily build of the Rosyln compiler

My new hypothesis is that the spec actually got tighter... and ex1 and ex2 should both fail, but they have not accounted for the ex2 syntax where its not triggering when it should (for the reason Marc G pointed out) so might be worth filing a bug report on this :-)

Abbotware
  • 1
  • 3
  • Yes, I am using C# 11 with .NET 7.0.100. Also see [Sharplab](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0ATEBqAPgAQCYBGAWACgB6SgAgFEEBDAWwAcAbGG4muGgM0YBLdgGcKRGgGEaAbwo1FNfAGYaAZVaMAdgB4h2gC4A+GgFkAFLH40DhmoygBzAJRyFSz5p36jp0VraANwenooBOjQAvDTaMADuGoG+JlYwNo6uIeRhSvgA7DQRwaE0AL4UFeRAA=) – Bartosz Dec 25 '22 at 15:31
  • OK. looks like linkpad uses preview in visual studio, it compiles if you use that setting, but not latest that leads me to believe this is a edge case in the current spec that they are planning to change / relax constraints for the case you pointed out – Abbotware Dec 25 '22 at 15:37
  • I added 'preview' to my project settings, but this doesn't influence the result. By the way, my VS Code (with the C# extension) doesn't recognize this error either. It occurs only after compiling with 'dotnet build'. – Bartosz Dec 25 '22 at 15:41
  • linqpad is able to run the code, so it seems like a compiler setting to enable the relaxed version of the lanugage spec needs to be turned on in the csproj file. that being said - it does seem slightly unusual to do what you are doing for just an int (I assume 'int' is a substitute for a struct to make the example repro simpler) ? – Abbotware Dec 25 '22 at 15:49
  • I was experimenting with ref structs and ref fields in C# 11, and this is a minimal example of a behavior that I don't understand. I expected that ref parameters/locals/returns/fields of non-ref struct type behave analogously to non-ref parameters/locals/returns/fields of ref struct type. This example shows that they behave differently. But you may be right that this is some edge case for which the rules are still being changed. – Bartosz Dec 25 '22 at 15:56
  • does this work for you? amazing that this produces no error, and yet your version does! [working syntax](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0ATEBqAPgAQCYBGAWACgB6SgAgFEEBDAWwAcAbGG4muGgM0YBLdgGcKRGgGEaAbwo1FNfAGYaAZVaMAdgB4h2gC4A+GgFkAFLH40DhmoygBzAJRyFSzwDdHNUVu0aAF4abRgAdw0A/SNjKxgbR1cAbg9PRXwAdj8A1PJPAF8KIvIgA=) def feels like the spec is not handling all edge cases – Abbotware Dec 25 '22 at 16:05
  • Yes, this works. This is Example 2 from my question. I understand why Examples 1 and 2 may behave differently—this is because the compiler needs to determine the "escape scope" of the local variable 'span' at the point of declaration. In Example 2, it can make it match the "escape scope" of 'arg', while in Example 1, it needs to fix it upfront. However, I don't understand why there is a difference between Example 1 and Example 3. – Bartosz Dec 25 '22 at 16:17
  • OK - i got linqpad to throw the error only if i use the latest daily roslyn compiler. My new hypothesis is that the spec actually got tighter... and ex1 and ex2 should both fail, but they have not accounted for the ex2 syntax where its not triggering when it should (for the reason Marc G pointed out) might be worth filing a bug report on this :-) – Abbotware Dec 25 '22 at 16:36
  • Marcs Code returns a compiler error inside `GetSpan` at the return clause unless the parameter in M is marked with `scoped`. In this case `GetSpan` has no error, but `M` cannot return its argument. – NineBerry Dec 25 '22 at 17:04
  • Unfortunately, Marc removed his code. It was something like: `Span GetSpan() { int x = 1; return M(ref x); }`. It seems to me that this method is an illustration of [this breaking change](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/breaking-changes/compiler%20breaking%20changes%20-%20dotnet%207#method-ref-struct-return-escape-analysis-depends-on-ref-escape-of-ref-arguments) in C# 11. It compiled successfully in C# 10, because it was impossible to write a method `M` that would put the reference to `x` into the span. But C# 11 made it possible, so `GetSpan` does not compile. – Bartosz Dec 25 '22 at 17:21
0

This very closely related issue had been reported to the Roslyn team before and closed as "by design".

The issue is that the compiler associates an internal scope with a variable of a ref struct type such as a span<>. This scope is decided at the moment the variable is declared.

Later on, when assignments happen, the internal scopes are compared.

Although both the uninitialized local span<> variable as well as the span<> variable wrapped around the ref argument should be returnable from the method, the compiler seems to think otherwise.

I would report this concrete example to the Roslyn team and see what they say about it.

Previous musings:


It is an issue of scope. Look at this more explicit example:

public void M()
{
    Span<int> spanOuter;
    {
        int answer = 42;
        spanOuter = new Span<int>(ref answer); // Compiler error
    }

    Console.WriteLine(spanOuter[0]); // Would access answer 42 which
                                     // is already out of scope
}

The new Span<int>() created has a narrower scope than the variable spanOuter. You cannot assign spans to another span with a broader scope because that could mean that the referenced data they hold is accessed after they don't exist any more. In this example, the answer variable goes out of scope before spanOuter[0] is accessed.

Let's remove the curly braces:

public void M()
{
    Span<int> spanOuter;
    int answer = 42;
    spanOuter = new Span<int>(ref answer); // Compiler error
    Console.WriteLine(spanOuter[0]); 
}

Now this should in theory work because the answer variable is still in scope at the Conole.WriteLine. The compiler still doesn't like it. Although there are no curly braces, the spanOuter variable still has a broader scope than at the new Span<int>() expression because its declaration happens on its own on a previous line.

When checking for breadth of scope, the compiler seems to be very strict and difference in scope just because of the separate variable declaration seems to be enough to not allow the assignment.


Even when we move the answer variable at the very beginning so that it basically has the same scope as an argument has, it is still not allowed.

public void M()
{
    int answer = 42;
    Span<int> spanOuter;
    spanOuter = new Span<int>(ref answer); // Compiler error
    Console.WriteLine(spanOuter[0]); 
}

The compiler seems to treat arguments just like local variables for this check. I agree that the compiler could be a bit more clever, look at the precise scope of the referenced data and allow some more cases, but it just doesn't do that.


Specifically, the compiler seems to have a special treatment when the target span variable is uninitialized as seen by the compiler.

public void M(ref int a)
{
    int answer = 42;

    Span<int> spanNull = null;
    Span<int> spanImplicitEmpty;
    Span<int> spanExplicitEmpty = Span<int>.Empty;
    Span<int> spanInitialized = new Span<int>(ref answer);

    Span<int> spanArgument = new Span<int>(ref a);

    spanNull            = spanArgument; // Compiler Error
    spanExplicitEmpty   = spanArgument; // Compiler Error
    spanImplicitEmpty   = spanArgument; // Compiler Error
    spanInitialized     = spanArgument; // Works
}

The same applies when using a return value:

public Span<int> M(ref int a)
{
    int answer = 42;

    Span<int> spanNull = null;
    Span<int> spanImplicitEmpty;
    Span<int> spanExplicitEmpty = Span<int>.Empty;
    Span<int> spanInitialized = new Span<int>(ref answer);
    
    Span<int> spanInitializedAndThenNull = new Span<int>(ref answer);
    spanInitializedAndThenNull = null;

    Span<int> spanArgument = new Span<int>(ref a);

    spanNull                    = spanArgument; // Compiler Error
    spanExplicitEmpty           = spanArgument; // Compiler Error
    spanImplicitEmpty           = spanArgument; // Compiler Error
    spanInitialized             = spanArgument; // Works
    spanInitializedAndThenNull  = spanArgument; // Works

    return spanArgument;
}
NineBerry
  • 26,306
  • 3
  • 62
  • 93
  • Your first example is clear, but is different from mine, because it tries to return a reference to a local variable. Your second example is similar to my Example 1, but is more understandable to me. The scope of 'spanOuter' is actually even broader—it is allowed to be returned. For example, change the return type from 'void' to 'Span', remove the last to lines, and add the following instead: 'spanOuter = default;' and 'return spanOuter'. It compiles successfully. – Bartosz Dec 25 '22 at 17:04
  • So your second example fails because it tries to assign a ref to a local variable to a span that can be returned. However, in my Example 1, a ref parameter is assigned to the span that we want to return. So I still don't understand why that doesn't work. – Bartosz Dec 25 '22 at 17:05
  • I've extended my answer a bit. I think the compiler makes a mistake. It specifically chokes only when the target span variable has never been assigned with a real span. – NineBerry Dec 25 '22 at 17:50
  • Your last example is very interesting. I understand that `spanInitialized = spanArgument;` works because the "escape scope" of `spanInitialized` is local (it captures a reference to a local variable). So the assignment tightens the "escape scope" of `spanArgument`, which is allowed. Notice that `spanInitialized` cannot be returned (assuming `Span` was the return type rather than `void`) even after the assignment. But I don't understand why the other three assignments produce a compile error. – Bartosz Dec 25 '22 at 18:12
  • OK, I will report it later today. Thanks! – Bartosz Dec 25 '22 at 19:14