diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs index 7bbb377a6b..3860a7874a 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs @@ -38,12 +38,23 @@ public void OutSpan(out Span span) public void Calls() { int value = 0; - Span span = CreateWithoutCapture(ref value); - //span = CreateAndCapture(ref value); -- would need scoped local, not yet implemented + scoped Span 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 diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs index d286f21cf8..ffa6501770 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs @@ -59,6 +59,7 @@ public enum Modifiers Async = 0x10000, Ref = 0x20000, Required = 0x40000, + Scoped = 0x80000, VisibilityMask = Private | Internal | Protected | Public, @@ -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, @@ -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: diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs index fe3c2e70eb..fc5cf64947 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs @@ -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) 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 CreateNewNode, string StepDescription)>(); @@ -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")); }