Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,23 @@ public void OutSpan(out Span<int> span)
public void Calls()
{
int value = 0;
Span<int> span = CreateWithoutCapture(ref value);
//span = CreateAndCapture(ref value); -- would need scoped local, not yet implemented
scoped Span<int> span = CreateWithoutCapture(ref value);
span = CreateAndCapture(ref value);
span = ScopedRefSpan(ref span);
span = ScopedSpan(span);
OutSpan(out span);
}

public int ReassignScopedRefToLocal(bool b, ref int x)
{
int num = 42;
scoped ref int reference = ref x;
if (b)
{
reference = ref num;
}
return reference;
}
}

internal ref struct RefFields
Expand Down
4 changes: 4 additions & 0 deletions ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public enum Modifiers
Async = 0x10000,
Ref = 0x20000,
Required = 0x40000,
Scoped = 0x80000,

VisibilityMask = Private | Internal | Protected | Public,

Expand All @@ -78,6 +79,7 @@ public static class CSharpModifiers
Modifiers.Unsafe,
Modifiers.Static, Modifiers.Abstract, Modifiers.Virtual, Modifiers.Sealed, Modifiers.Override,
Modifiers.Required, Modifiers.Readonly, Modifiers.Volatile,
Modifiers.Scoped,
Modifiers.Ref,
Modifiers.Extern, Modifiers.Partial, Modifiers.Const,
Modifiers.Async,
Expand Down Expand Up @@ -124,6 +126,8 @@ public static string GetModifierName(Modifiers modifier)
return "async";
case Modifiers.Ref:
return "ref";
case Modifiers.Scoped:
return "scoped";
case Modifiers.Required:
return "required";
case Modifiers.Any:
Expand Down
59 changes: 59 additions & 0 deletions ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,58 @@ bool CombineDeclarationAndInitializer(VariableToDeclare v, TransformContext cont
return !context.Settings.SeparateLocalVariableDeclarations;
}

// A by-ref-like local (a 'ref' local or a ref struct such as Span<T>) that is re-assigned to a value
// limited to a method-local's scope must have been declared 'scoped' for the original source to pass
// ref-safety (otherwise CS8374 for ref locals, or CS8347/CS8352 for ref structs). 'scoped' has no runtime effect, and such a local provably
// cannot escape the method, so restoring it is behavior-preserving and lets the code recompile.
static bool RequiresScopedModifier(ILVariable variable)
{
if (variable.StoreInstructions.Count < 2)
return false;
foreach (var store in variable.StoreInstructions)
{
if (store is StLoc stloc && CapturesNarrowReference(stloc.Value))
return true;
}
return false;
}

// True if the stored ref / ref-struct value is limited to a method-local's scope: either the address
// of method-local storage (a narrow ref itself), or a call/newobj that passes such an address to a
// NON-scoped by-ref parameter (so the returned ref struct captures it and inherits the narrow scope).
static bool CapturesNarrowReference(ILInstruction value)
{
switch (value)
{
case LdLoca:
case LdFlda:
return IsAddressOfMethodLocal(value);
case CallInstruction call when call.OpCode is OpCode.Call or OpCode.CallVirt or OpCode.NewObj:
for (int i = 0; i < call.Arguments.Count; i++)
{
if (IsAddressOfMethodLocal(call.Arguments[i])
&& call.GetParameter(i) is { ReferenceKind: ReferenceKind.Ref or ReferenceKind.In or ReferenceKind.RefReadOnly } p
&& !p.Lifetime.ScopedRef)
{
return true;
}
}
return false;
default:
return false;
}
}

// True if 'addr' is the address of method-local storage: the address of a local/stack-slot, or a
// field thereof (a struct field of a local is exactly as narrow as the local). A field of a heap
// object (LdFlda over a loaded reference) or an array element bottoms out at a non-LdLoca root and
// is therefore treated as wide.
static bool IsAddressOfMethodLocal(ILInstruction addr) => addr switch {
LdLoca ldloca => ldloca.Variable.Kind is VariableKind.Local or VariableKind.StackSlot,
LdFlda ldflda => IsAddressOfMethodLocal(ldflda.Target),
_ => false,
};

void InsertVariableDeclarations(TransformContext context)
{
var replacements = new List<(AstNode OldNode, Func<AstNode> CreateNewNode, string StepDescription)>();
Expand Down Expand Up @@ -638,6 +690,13 @@ void InsertVariableDeclarations(TransformContext context)
init.AddAnnotation(annotation);
}
}
if (context.Settings.ScopedRef
&& v.ILVariable.Kind is VariableKind.Local or VariableKind.StackSlot
&& (type is ComposedType { HasRefSpecifier: true } || v.ILVariable.Type.IsByRefLike)
&& RequiresScopedModifier(v.ILVariable))
{
vds.Modifiers |= Modifiers.Scoped;
}
return vds;
}, "Combine variable declaration with initializer"));
}
Expand Down
Loading