Skip to content

Decompiling a scoped ref local or ref-struct local drops the modifier (does not recompile) #3853

Description

@sailro

Summary

scoped on a local variable — a scoped ref T local, or a ref-struct local such as scoped Span<T> — is a compile-time ref-safety annotation with no runtime effect. Unlike scoped on a parameter (which is encoded as ScopedRefAttribute and correctly recovered by ILSpy), scoped on a local is not encoded in IL, metadata, or the PDB, so the decompiler drops it. When the local is later reassigned a value with a narrower escape scope, the decompiled output no longer recompiles: CS8374 for ref locals, CS8347 for ref-struct locals.

Input code

Compiled in Release targeting .NET 8.0 (AllowUnsafeBlocks, C# 11+):

using System;

public class C
{
    // A 'scoped' ref local, ref-reassigned to the address of a method-local.
    public static int RefLocal(bool b, ref int x)
    {
        int y = 42;
        scoped ref int r = ref x;
        if (b)
        {
            r = ref y;
        }
        return r;
    }

    // A 'scoped' ref-struct local, reassigned to a call that captures a method-local.
    private static int f;
    private static Span<int> NoCapture(scoped ref int v) => new Span<int>(ref f);
    private static Span<int> Capture(ref int v) => new Span<int>(ref v);

    public static int RefStruct(bool b)
    {
        int value = 7;
        scoped Span<int> span = NoCapture(ref value);
        if (b)
        {
            span = Capture(ref value);
        }
        return span[0];
    }
}

Erroneous output

The scoped modifier is dropped from both locals:

public static int RefLocal(bool b, ref int x)
{
    int num = 42;
    ref int reference = ref x;          // <-- 'scoped' dropped
    if (b)
    {
        reference = ref num;
    }
    return reference;
}

public static int RefStruct(bool b)
{
    int v = 7;
    Span<int> span = NoCapture(ref v);  // <-- 'scoped' dropped
    if (b)
    {
        span = Capture(ref v);
    }
    return span[0];
}

Recompiling this output fails:

error CS8374: Cannot ref-assign 'num' to 'reference' because 'num' has a narrower escape scope than 'reference'.
error CS8347: Cannot use a result of 'C.Capture(ref int)' in this context because it may expose variables referenced by parameter 'v' outside of their declaration scope

Expected output

scoped is restored, so the output recompiles. Because scoped has no runtime effect, this is behavior-preserving:

scoped ref int reference = ref x;
// ...
scoped Span<int> span = NoCapture(ref v);

Details

  • Product in use: ICSharpCode.Decompiler
  • Version in use: master 6b2852ce (11.x); also reproduces on 10.1 (10.1.0.8386).
  • Root cause: scoped on a local is fully erased at compile time — identical IL, no attribute, and no PDB entry (verified: ilspycmd -usepdb recovers local names but not scoped-ness). ILSpy models scoped only on IParameter (via ScopedRefAttribute); VariableDeclarationStatement has no scoped concept. Recovering it therefore requires inferring, from the local's reassignments, that it must have been scoped for the original source to pass ref-safety.
  • I'm happy to contribute a fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions