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.
Summary
scopedon a local variable — ascoped ref Tlocal, or a ref-struct local such asscoped Span<T>— is a compile-time ref-safety annotation with no runtime effect. Unlikescopedon a parameter (which is encoded asScopedRefAttributeand correctly recovered by ILSpy),scopedon 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 forreflocals, CS8347 for ref-struct locals.Input code
Compiled in Release targeting .NET 8.0 (
AllowUnsafeBlocks, C# 11+):Erroneous output
The
scopedmodifier is dropped from both locals:Recompiling this output fails:
Expected output
scopedis restored, so the output recompiles. Becausescopedhas no runtime effect, this is behavior-preserving:Details
6b2852ce(11.x); also reproduces on 10.1 (10.1.0.8386).scopedon a local is fully erased at compile time — identical IL, no attribute, and no PDB entry (verified:ilspycmd -usepdbrecovers local names but not scoped-ness). ILSpy modelsscopedonly onIParameter(viaScopedRefAttribute);VariableDeclarationStatementhas noscopedconcept. Recovering it therefore requires inferring, from the local's reassignments, that it must have beenscopedfor the original source to pass ref-safety.