mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Roslyn passes a display class into a local function by ref, and a local function that only forwards that parameter to a sibling has no closure variable of its own. The closure analysis therefore found nothing to anchor it and fell back to the root method body, which put it out of reach of the callees it forwards to; CallBuilder then hit the assert guarding a local function reference it cannot resolve and emitted the raw metadata name of the target instead. The constructor path also mixed use-site containers into a scope the closure analysis had already determined; when the use-sites live in separate function bodies there is no common container, and resetting to the constructor body threw that scope away. Assisted-by: Claude:claude-opus-5:Claude Code
935 lines
36 KiB
C#
935 lines
36 KiB
C#
// Copyright (c) 2019 Siegfried Pammer
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
// software and associated documentation files (the "Software"), to deal in the Software
|
|
// without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
// to whom the Software is furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in all copies or
|
|
// substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
// DEALINGS IN THE SOFTWARE.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Reflection.Metadata;
|
|
using System.Text.RegularExpressions;
|
|
|
|
using ICSharpCode.Decompiler.CSharp;
|
|
using ICSharpCode.Decompiler.Metadata;
|
|
using ICSharpCode.Decompiler.TypeSystem;
|
|
using ICSharpCode.Decompiler.TypeSystem.Implementation;
|
|
using ICSharpCode.Decompiler.Util;
|
|
|
|
namespace ICSharpCode.Decompiler.IL.Transforms
|
|
{
|
|
/// <summary>
|
|
/// Decompiler step for C# 7.0 local functions
|
|
/// </summary>
|
|
public class LocalFunctionDecompiler : IILTransform
|
|
{
|
|
ILTransformContext context;
|
|
ITypeResolveContext resolveContext;
|
|
|
|
struct LocalFunctionInfo
|
|
{
|
|
public List<ILInstruction> UseSites;
|
|
public IMethod Method;
|
|
public ILFunction Definition;
|
|
/// <summary>
|
|
/// Used to store all synthesized call-site arguments grouped by the parameter index.
|
|
/// We use a dictionary instead of a simple array, because -1 is used for the this parameter
|
|
/// and there might be many non-synthesized arguments in between.
|
|
/// </summary>
|
|
public Dictionary<int, List<ILInstruction>> LocalFunctionArguments;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The transform works like this:
|
|
///
|
|
/// <para>
|
|
/// local functions can either be used in method calls, i.e., call and callvirt instructions,
|
|
/// or can be used as part of the "delegate construction" pattern, i.e.,
|
|
/// <c>newobj Delegate(<target-expression>, ldftn <method>)</c>.
|
|
/// </para>
|
|
/// As local functions can be declared practically anywhere, we have to take a look at
|
|
/// all use-sites and infer the declaration location from that. Use-sites can be call,
|
|
/// callvirt and ldftn instructions.
|
|
/// After all use-sites are collected we construct the ILAst of the local function
|
|
/// and add it to the parent function.
|
|
/// Then all use-sites of the local-function are transformed to a call to the
|
|
/// <c>LocalFunctionMethod</c> or a ldftn of the <c>LocalFunctionMethod</c>.
|
|
/// In a next step we handle all nested local functions.
|
|
/// After all local functions are transformed, we move all local functions that capture
|
|
/// any variables to their respective declaration scope.
|
|
/// </summary>
|
|
public void Run(ILFunction function, ILTransformContext context)
|
|
{
|
|
if (!context.Settings.LocalFunctions)
|
|
return;
|
|
// Disable the transform if we are decompiling a display-class or local function method:
|
|
// This happens if a local function or display class is selected in the ILSpy tree view.
|
|
if (IsLocalFunctionMethod(function.Method, context) || IsLocalFunctionDisplayClass(
|
|
function.Method.ParentModule.MetadataFile,
|
|
(TypeDefinitionHandle)function.Method.DeclaringTypeDefinition.MetadataToken,
|
|
context)
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
|
|
this.context = context;
|
|
this.resolveContext = new SimpleTypeResolveContext(function.Method);
|
|
var localFunctions = new Dictionary<MethodDefinitionHandle, LocalFunctionInfo>();
|
|
// Find all local functions declared inside this method, including nested local functions or local functions declared in lambdas.
|
|
FindUseSites(function, context, localFunctions);
|
|
ReplaceReferencesToDisplayClassThis(localFunctions.Values);
|
|
DetermineCaptureAndDeclarationScopes(localFunctions.Values);
|
|
PropagateClosureParameterArguments(localFunctions);
|
|
TransformUseSites(localFunctions.Values);
|
|
}
|
|
|
|
private void ReplaceReferencesToDisplayClassThis(Dictionary<MethodDefinitionHandle, LocalFunctionInfo>.ValueCollection localFunctions)
|
|
{
|
|
foreach (var info in localFunctions)
|
|
{
|
|
var localFunction = info.Definition;
|
|
if (localFunction.Method.IsStatic)
|
|
continue;
|
|
var thisVar = localFunction.Variables.SingleOrDefault(VariableKindExtensions.IsThis);
|
|
if (thisVar == null)
|
|
continue;
|
|
var compatibleArgument = FindCompatibleArgument(
|
|
info,
|
|
info.UseSites.OfType<CallInstruction>().Select(u => u.Arguments[0]).ToArray(),
|
|
ignoreStructure: true
|
|
);
|
|
if (compatibleArgument == null)
|
|
continue;
|
|
context.Step($"Replace 'this' with {compatibleArgument}", localFunction);
|
|
localFunction.AcceptVisitor(new DelegateConstruction.ReplaceDelegateTargetVisitor(compatibleArgument, thisVar));
|
|
DetermineCaptureAndDeclarationScope(info, -1, compatibleArgument);
|
|
}
|
|
}
|
|
|
|
private void DetermineCaptureAndDeclarationScopes(Dictionary<MethodDefinitionHandle, LocalFunctionInfo>.ValueCollection localFunctions)
|
|
{
|
|
foreach (var info in localFunctions)
|
|
{
|
|
context.CancellationToken.ThrowIfCancellationRequested();
|
|
if (info.Definition == null)
|
|
{
|
|
context.Function.Warnings.Add($"Could not decode local function '{info.Method}'");
|
|
continue;
|
|
}
|
|
|
|
context.StepStartGroup($"Determine and move to declaration scope of " + info.Definition.Name, info.Definition);
|
|
try
|
|
{
|
|
var localFunction = info.Definition;
|
|
|
|
foreach (var useSite in info.UseSites)
|
|
{
|
|
DetermineCaptureAndDeclarationScope(info, useSite);
|
|
}
|
|
|
|
if (context.Function.Method.IsConstructor)
|
|
{
|
|
// Local functions reached from a field initializer usually capture nothing, so
|
|
// the closure analysis leaves no scope behind; the innermost block containing
|
|
// all use-sites is a better place for them than the whole constructor body.
|
|
// Use-sites spread over separate function bodies have no common block
|
|
// container at all; there the scope from the closure analysis stands.
|
|
BlockContainer useSiteScope = null;
|
|
foreach (var useSite in info.UseSites)
|
|
{
|
|
useSiteScope = useSiteScope == null
|
|
? BlockContainer.FindClosestContainer(useSite)
|
|
: FindCommonAncestorInstruction<BlockContainer>(useSite, useSiteScope);
|
|
if (useSiteScope == null)
|
|
break;
|
|
}
|
|
localFunction.DeclarationScope = useSiteScope
|
|
?? localFunction.DeclarationScope
|
|
?? (BlockContainer)context.Function.Body;
|
|
}
|
|
|
|
if (localFunction.DeclarationScope == null)
|
|
{
|
|
localFunction.DeclarationScope = (BlockContainer)context.Function.Body;
|
|
}
|
|
|
|
ILFunction declaringFunction = GetDeclaringFunction(localFunction);
|
|
if (declaringFunction != context.Function)
|
|
{
|
|
context.Step($"Move {localFunction.Name} from {context.Function.Name} to {declaringFunction.Name}", localFunction);
|
|
context.Function.LocalFunctions.Remove(localFunction);
|
|
declaringFunction.LocalFunctions.Add(localFunction);
|
|
}
|
|
|
|
if (TryValidateSkipCount(info, out int skipCount) && skipCount != localFunction.ReducedMethod.NumberOfCompilerGeneratedTypeParameters)
|
|
{
|
|
Debug.Assert(false);
|
|
context.Function.Warnings.Add($"Could not decode local function '{info.Method}'");
|
|
if (declaringFunction != context.Function)
|
|
{
|
|
declaringFunction.LocalFunctions.Remove(localFunction);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
context.StepEndGroup(keepIfEmpty: true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TransformUseSites(Dictionary<MethodDefinitionHandle, LocalFunctionInfo>.ValueCollection localFunctions)
|
|
{
|
|
foreach (var info in localFunctions)
|
|
{
|
|
context.CancellationToken.ThrowIfCancellationRequested();
|
|
if (info.Definition == null)
|
|
continue;
|
|
context.StepStartGroup($"TransformUseSites of " + info.Definition.Name, info.Definition);
|
|
try
|
|
{
|
|
foreach (var useSite in info.UseSites)
|
|
{
|
|
context.Step($"Transform use-site at IL_{useSite.StartILOffset:x4}", useSite);
|
|
switch (useSite)
|
|
{
|
|
case NewObj newObj:
|
|
TransformToLocalFunctionReference(info.Definition, newObj);
|
|
break;
|
|
case CallInstruction call:
|
|
var callReplacement = TransformToLocalFunctionInvocation(info.Definition.ReducedMethod, call);
|
|
context.EndStep(callReplacement);
|
|
break;
|
|
case LdFtn fnptr:
|
|
var specializeMethod = info.Definition.ReducedMethod
|
|
.Specialize(fnptr.Method.Substitution);
|
|
var replacement = new LdFtn(specializeMethod).WithILRange(fnptr);
|
|
fnptr.ReplaceWith(replacement);
|
|
context.EndStep(replacement);
|
|
break;
|
|
default:
|
|
throw new NotSupportedException();
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
context.StepEndGroup();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void PropagateClosureParameterArguments(Dictionary<MethodDefinitionHandle, LocalFunctionInfo> localFunctions)
|
|
{
|
|
foreach (var localFunction in context.Function.Descendants.OfType<ILFunction>())
|
|
{
|
|
if (localFunction.Kind != ILFunctionKind.LocalFunction)
|
|
continue;
|
|
context.CancellationToken.ThrowIfCancellationRequested();
|
|
var token = (MethodDefinitionHandle)localFunction.Method.MetadataToken;
|
|
var info = localFunctions[token];
|
|
|
|
foreach (var useSite in info.UseSites)
|
|
{
|
|
switch (useSite)
|
|
{
|
|
case NewObj newObj:
|
|
AddAsArgument(-1, newObj.Arguments[0]);
|
|
break;
|
|
case CallInstruction call:
|
|
int firstArgumentIndex;
|
|
if (info.Method.IsStatic)
|
|
{
|
|
firstArgumentIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
firstArgumentIndex = 1;
|
|
AddAsArgument(-1, call.Arguments[0]);
|
|
}
|
|
for (int i = call.Arguments.Count - 1; i >= firstArgumentIndex; i--)
|
|
{
|
|
AddAsArgument(i - firstArgumentIndex, call.Arguments[i]);
|
|
}
|
|
break;
|
|
case LdFtn _:
|
|
// &LocalFunction is only possible, if the local function is declared static,
|
|
// this means that the local function can be declared in the top-level scope.
|
|
// Thus, there are no closure parameters that need propagation.
|
|
break;
|
|
default:
|
|
throw new NotSupportedException();
|
|
}
|
|
}
|
|
|
|
context.StepStartGroup($"PropagateClosureParameterArguments of " + info.Definition.Name, info.Definition);
|
|
try
|
|
{
|
|
foreach (var (index, arguments) in info.LocalFunctionArguments)
|
|
{
|
|
var targetVariable = info.Definition.Variables.SingleOrDefault(p => p.Kind == VariableKind.Parameter && p.Index == index);
|
|
if (targetVariable == null)
|
|
continue;
|
|
var compatibleArgument = FindCompatibleArgument(info, arguments);
|
|
if (compatibleArgument == null)
|
|
continue;
|
|
context.Step($"Replace '{targetVariable}' with '{compatibleArgument}'", info.Definition);
|
|
info.Definition.AcceptVisitor(new DelegateConstruction.ReplaceDelegateTargetVisitor(compatibleArgument, targetVariable));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
context.StepEndGroup(keepIfEmpty: true);
|
|
}
|
|
|
|
void AddAsArgument(int index, ILInstruction argument)
|
|
{
|
|
switch (argument)
|
|
{
|
|
case LdLoc _:
|
|
case LdLoca _:
|
|
case LdFlda _:
|
|
case LdObj _:
|
|
if (index >= 0 && !IsClosureParameter(info.Method.Parameters[index], resolveContext))
|
|
return;
|
|
break;
|
|
default:
|
|
if (index >= 0 && IsClosureParameter(info.Method.Parameters[index], resolveContext))
|
|
info.Definition.Warnings.Add("Could not transform parameter " + index + ": unsupported argument pattern");
|
|
return;
|
|
}
|
|
|
|
if (!info.LocalFunctionArguments.TryGetValue(index, out var arguments))
|
|
{
|
|
arguments = new List<ILInstruction>();
|
|
info.LocalFunctionArguments.Add(index, arguments);
|
|
}
|
|
arguments.Add(argument);
|
|
}
|
|
}
|
|
}
|
|
|
|
private ILInstruction FindCompatibleArgument(LocalFunctionInfo info, IList<ILInstruction> arguments, bool ignoreStructure = false)
|
|
{
|
|
foreach (var arg in arguments)
|
|
{
|
|
if (arg is IInstructionWithVariableOperand ld2 && (ignoreStructure || info.Definition.IsDescendantOf(ld2.Variable.Function)))
|
|
return arg;
|
|
var v = ResolveAncestorScopeReference(arg);
|
|
if (v != null)
|
|
return new LdLoc(v);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private ILVariable ResolveAncestorScopeReference(ILInstruction inst)
|
|
{
|
|
if (!inst.MatchLdFld(out var target, out var field))
|
|
return null;
|
|
if (field.Type.Kind != TypeKind.Class)
|
|
return null;
|
|
if (!(TransformDisplayClassUsage.IsPotentialClosure(context, field.Type.GetDefinition()) || context.Function.Method.DeclaringType.Equals(field.Type)))
|
|
return null;
|
|
return FindClosureVariableOfType(field.Type);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the variable holding the display class instance of the given type, anywhere in the
|
|
/// function tree currently being decompiled.
|
|
/// </summary>
|
|
private ILVariable FindClosureVariableOfType(IType type)
|
|
{
|
|
foreach (var v in context.Function.Descendants.OfType<ILFunction>().Prepend(context.Function).SelectMany(f => f.Variables))
|
|
{
|
|
if (!(TransformDisplayClassUsage.IsClosure(context, v, out var varType, out _) && varType.Equals(type)))
|
|
continue;
|
|
return v;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private ILFunction GetDeclaringFunction(ILFunction localFunction)
|
|
{
|
|
if (localFunction.DeclarationScope == null)
|
|
return null;
|
|
ILInstruction inst = localFunction.DeclarationScope;
|
|
while (inst != null)
|
|
{
|
|
if (inst is ILFunction declaringFunction)
|
|
return declaringFunction;
|
|
inst = inst.Parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
bool TryValidateSkipCount(LocalFunctionInfo info, out int skipCount)
|
|
{
|
|
skipCount = 0;
|
|
var localFunction = info.Definition;
|
|
if (localFunction.Method.TypeParameters.Count == 0)
|
|
return true;
|
|
var parentMethod = ((ILFunction)localFunction.Parent).Method;
|
|
var method = localFunction.Method;
|
|
skipCount = parentMethod.DeclaringType.TypeParameterCount - method.DeclaringType.TypeParameterCount;
|
|
|
|
if (skipCount > 0)
|
|
return false;
|
|
skipCount += parentMethod.TypeParameters.Count;
|
|
if (skipCount < 0 || skipCount > method.TypeArguments.Count)
|
|
return false;
|
|
|
|
if (skipCount > 0)
|
|
{
|
|
#if DEBUG
|
|
foreach (var useSite in info.UseSites)
|
|
{
|
|
var callerMethod = useSite.Ancestors.OfType<ILFunction>().First().Method;
|
|
callerMethod = callerMethod.ReducedFrom ?? callerMethod;
|
|
IMethod m;
|
|
switch (useSite)
|
|
{
|
|
case NewObj newObj:
|
|
m = ((LdFtn)newObj.Arguments[1]).Method;
|
|
break;
|
|
case CallInstruction call:
|
|
m = call.Method;
|
|
break;
|
|
case LdFtn fnptr:
|
|
m = fnptr.Method;
|
|
break;
|
|
default:
|
|
throw new NotSupportedException();
|
|
}
|
|
var totalSkipCount = skipCount + m.DeclaringType.TypeParameterCount;
|
|
var methodSkippedArgs = m.DeclaringType.TypeArguments.Concat(m.TypeArguments).Take(totalSkipCount);
|
|
Debug.Assert(methodSkippedArgs.SequenceEqual(callerMethod.DeclaringType.TypeArguments.Concat(callerMethod.TypeArguments).Take(totalSkipCount)));
|
|
Debug.Assert(methodSkippedArgs.All(p => p.Kind == TypeKind.TypeParameter));
|
|
Debug.Assert(methodSkippedArgs.Select(p => p.Name).SequenceEqual(m.DeclaringType.TypeParameters.Concat(m.TypeParameters).Take(totalSkipCount).Select(p => p.Name)));
|
|
}
|
|
#endif
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void FindUseSites(ILFunction function, ILTransformContext context, Dictionary<MethodDefinitionHandle, LocalFunctionInfo> localFunctions)
|
|
{
|
|
foreach (var inst in function.Body.Descendants)
|
|
{
|
|
context.CancellationToken.ThrowIfCancellationRequested();
|
|
if (inst is CallInstruction call && !call.Method.IsLocalFunction && IsLocalFunctionMethod(call.Method, context))
|
|
{
|
|
HandleUseSite(call.Method, call);
|
|
}
|
|
else if (inst is LdFtn ldftn && !ldftn.Method.IsLocalFunction && IsLocalFunctionMethod(ldftn.Method, context))
|
|
{
|
|
if (ldftn.Parent is NewObj newObj && DelegateConstruction.MatchDelegateConstruction(newObj, out _, out _, out _))
|
|
HandleUseSite(ldftn.Method, newObj);
|
|
else
|
|
HandleUseSite(ldftn.Method, ldftn);
|
|
}
|
|
}
|
|
|
|
void HandleUseSite(IMethod targetMethod, ILInstruction inst)
|
|
{
|
|
if (!localFunctions.TryGetValue((MethodDefinitionHandle)targetMethod.MetadataToken, out var info))
|
|
{
|
|
context.StepStartGroup($"Read local function '{targetMethod.Name}'", inst);
|
|
info = new LocalFunctionInfo() {
|
|
UseSites = new List<ILInstruction>() { inst },
|
|
LocalFunctionArguments = new Dictionary<int, List<ILInstruction>>(),
|
|
Method = (IMethod)targetMethod.MemberDefinition,
|
|
};
|
|
var rootFunction = context.Function;
|
|
int skipCount = GetSkipCount(rootFunction, targetMethod);
|
|
info.Definition = ReadLocalFunctionDefinition(rootFunction, targetMethod, skipCount);
|
|
localFunctions.Add((MethodDefinitionHandle)targetMethod.MetadataToken, info);
|
|
if (info.Definition != null)
|
|
{
|
|
FindUseSites(info.Definition, context, localFunctions);
|
|
}
|
|
context.StepEndGroup();
|
|
}
|
|
else
|
|
{
|
|
info.UseSites.Add(inst);
|
|
}
|
|
}
|
|
}
|
|
|
|
ILFunction ReadLocalFunctionDefinition(ILFunction rootFunction, IMethod targetMethod, int skipCount)
|
|
{
|
|
var methodDefinition = context.PEFile.Metadata.GetMethodDefinition((MethodDefinitionHandle)targetMethod.MetadataToken);
|
|
var genericContext = GenericContextFromTypeArguments(targetMethod, skipCount);
|
|
if (genericContext == null)
|
|
return null;
|
|
ILFunction function;
|
|
bool hasBody = methodDefinition.HasBody();
|
|
if (!hasBody)
|
|
{
|
|
function = new ILFunction(targetMethod, 0,
|
|
new TypeSystem.GenericContext(genericContext?.ClassTypeParameters, genericContext?.MethodTypeParameters),
|
|
new Nop(), ILFunctionKind.LocalFunction);
|
|
}
|
|
else
|
|
{
|
|
var ilReader = context.CreateILReader();
|
|
var body = context.PEFile.GetMethodBody(methodDefinition.RelativeVirtualAddress);
|
|
|
|
function = ilReader.ReadIL((MethodDefinitionHandle)targetMethod.MetadataToken, body,
|
|
genericContext.GetValueOrDefault(), ILFunctionKind.LocalFunction,
|
|
context.CancellationToken);
|
|
}
|
|
// Embed the local function into the parent function's ILAst, so that "Show steps" can show
|
|
// how the local function body is being transformed.
|
|
rootFunction.LocalFunctions.Add(function);
|
|
if (hasBody)
|
|
{
|
|
function.DeclarationScope = (BlockContainer)rootFunction.Body;
|
|
function.CheckInvariant(ILPhase.Normal, context.TypeSystem);
|
|
var nestedContext = new ILTransformContext(context, function);
|
|
function.RunTransforms(CSharpDecompiler.GetILTransforms().TakeWhile(t => !(t is LocalFunctionDecompiler)), nestedContext);
|
|
function.DeclarationScope = null;
|
|
}
|
|
function.ReducedMethod = ReduceToLocalFunction(function.Method, skipCount);
|
|
return function;
|
|
}
|
|
|
|
int GetSkipCount(ILFunction rootFunction, IMethod targetMethod)
|
|
{
|
|
targetMethod = (IMethod)targetMethod.MemberDefinition;
|
|
var skipCount = rootFunction.Method.DeclaringType.TypeParameters.Count + rootFunction.Method.TypeParameters.Count - targetMethod.DeclaringType.TypeParameters.Count;
|
|
if (skipCount < 0)
|
|
{
|
|
skipCount = 0;
|
|
}
|
|
if (targetMethod.TypeParameters.Count > 0)
|
|
{
|
|
var lastParams = targetMethod.Parameters.Where(p => IsClosureParameter(p, this.resolveContext)).SelectMany(p => p.Type.UnwrapByRef().TypeArguments)
|
|
.Select(pt => (int?)targetMethod.TypeParameters.IndexOf(pt)).DefaultIfEmpty().Max();
|
|
if (lastParams != null && lastParams.GetValueOrDefault() + 1 > skipCount)
|
|
skipCount = lastParams.GetValueOrDefault() + 1;
|
|
}
|
|
return skipCount;
|
|
}
|
|
|
|
static TypeSystem.GenericContext? GenericContextFromTypeArguments(IMethod targetMethod, int skipCount)
|
|
{
|
|
if (skipCount < 0 || skipCount > targetMethod.TypeParameters.Count)
|
|
{
|
|
Debug.Assert(false);
|
|
return null;
|
|
}
|
|
int total = targetMethod.DeclaringType.TypeParameters.Count + skipCount;
|
|
if (total == 0)
|
|
return default(TypeSystem.GenericContext);
|
|
|
|
var classTypeParameters = new List<ITypeParameter>(targetMethod.DeclaringType.TypeParameters);
|
|
var methodTypeParameters = new List<ITypeParameter>(targetMethod.TypeParameters);
|
|
var skippedTypeArguments = targetMethod.DeclaringType.TypeArguments.Concat(targetMethod.TypeArguments).Take(total);
|
|
int idx = 0;
|
|
foreach (var skippedTA in skippedTypeArguments)
|
|
{
|
|
int curIdx;
|
|
List<ITypeParameter> curParameters;
|
|
IReadOnlyList<IType> curArgs;
|
|
if (idx < classTypeParameters.Count)
|
|
{
|
|
curIdx = idx;
|
|
curParameters = classTypeParameters;
|
|
curArgs = targetMethod.DeclaringType.TypeArguments;
|
|
}
|
|
else
|
|
{
|
|
curIdx = idx - classTypeParameters.Count;
|
|
curParameters = methodTypeParameters;
|
|
curArgs = targetMethod.TypeArguments;
|
|
}
|
|
if (curArgs[curIdx].Kind != TypeKind.TypeParameter)
|
|
break;
|
|
curParameters[curIdx] = (ITypeParameter)skippedTA;
|
|
idx++;
|
|
}
|
|
if (idx != total)
|
|
{
|
|
Debug.Assert(false);
|
|
return null;
|
|
}
|
|
|
|
return new TypeSystem.GenericContext(classTypeParameters, methodTypeParameters);
|
|
}
|
|
|
|
static T FindCommonAncestorInstruction<T>(ILInstruction a, ILInstruction b)
|
|
where T : ILInstruction
|
|
{
|
|
var ancestorsOfB = new HashSet<T>(b.Ancestors.OfType<T>());
|
|
return a.Ancestors.OfType<T>().FirstOrDefault(ancestorsOfB.Contains);
|
|
}
|
|
|
|
internal static bool IsClosureParameter(IParameter parameter, ITypeResolveContext context)
|
|
{
|
|
return IsClosureParameter(parameter, context.CurrentTypeDefinition);
|
|
}
|
|
|
|
internal static bool IsClosureParameter(IParameter parameter, ITypeDefinition currentTypeDefinition)
|
|
{
|
|
if (parameter.Type is not ByReferenceType brt)
|
|
return false;
|
|
var type = brt.ElementType.GetDefinition();
|
|
return type != null
|
|
&& type.Kind == TypeKind.Struct
|
|
&& TransformDisplayClassUsage.IsPotentialClosure(currentTypeDefinition, type);
|
|
}
|
|
|
|
LocalFunctionMethod ReduceToLocalFunction(IMethod method, int typeParametersToRemove)
|
|
{
|
|
int parametersToRemove = 0;
|
|
for (int i = method.Parameters.Count - 1; i >= 0; i--)
|
|
{
|
|
if (!IsClosureParameter(method.Parameters[i], resolveContext))
|
|
break;
|
|
parametersToRemove++;
|
|
}
|
|
return new LocalFunctionMethod(method, method.Name, CanBeStaticLocalFunction(), parametersToRemove, typeParametersToRemove);
|
|
|
|
bool CanBeStaticLocalFunction()
|
|
{
|
|
if (!context.Settings.StaticLocalFunctions)
|
|
return false;
|
|
// Cannot be static because there are closure parameters that will be removed
|
|
if (parametersToRemove > 0)
|
|
return false;
|
|
// no closure parameters, but static:
|
|
// we can safely assume, this local function can be declared static
|
|
if (method.IsStatic)
|
|
return true;
|
|
// the local function is used in conjunction with a lambda, which means,
|
|
// it is defined inside the display-class type
|
|
var declaringType = method.DeclaringTypeDefinition;
|
|
if (!declaringType.IsCompilerGenerated())
|
|
return false;
|
|
// if there are no instance fields, we can make it a static local function
|
|
return !declaringType.GetFields(f => !f.IsStatic).Any();
|
|
}
|
|
}
|
|
|
|
static void TransformToLocalFunctionReference(ILFunction function, CallInstruction useSite)
|
|
{
|
|
ILInstruction target = useSite.Arguments[0];
|
|
target.ReplaceWith(new LdNull().WithILRange(target));
|
|
if (target is IInstructionWithVariableOperand withVar && withVar.Variable.Kind == VariableKind.Local)
|
|
{
|
|
withVar.Variable.Kind = VariableKind.DisplayClassLocal;
|
|
}
|
|
var fnptr = (IInstructionWithMethodOperand)useSite.Arguments[1];
|
|
var specializeMethod = function.ReducedMethod.Specialize(fnptr.Method.Substitution);
|
|
var replacement = new LdFtn(specializeMethod).WithILRange((ILInstruction)fnptr);
|
|
useSite.Arguments[1].ReplaceWith(replacement);
|
|
}
|
|
|
|
Call TransformToLocalFunctionInvocation(LocalFunctionMethod reducedMethod, CallInstruction useSite)
|
|
{
|
|
var specializeMethod = reducedMethod.Specialize(useSite.Method.Substitution);
|
|
bool wasInstanceCall = !useSite.Method.IsStatic;
|
|
var replacement = new Call(specializeMethod);
|
|
int firstArgumentIndex = wasInstanceCall ? 1 : 0;
|
|
int argumentCount = useSite.Arguments.Count;
|
|
int reducedArgumentCount = argumentCount - (reducedMethod.NumberOfCompilerGeneratedParameters + firstArgumentIndex);
|
|
replacement.Arguments.AddRange(useSite.Arguments.Skip(firstArgumentIndex).Take(reducedArgumentCount));
|
|
// copy flags
|
|
replacement.ConstrainedTo = useSite.ConstrainedTo;
|
|
replacement.ILStackWasEmpty = useSite.ILStackWasEmpty;
|
|
replacement.IsTail = useSite.IsTail;
|
|
// copy IL ranges
|
|
replacement.AddILRange(useSite);
|
|
if (wasInstanceCall)
|
|
{
|
|
replacement.AddILRange(useSite.Arguments[0]);
|
|
if (useSite.Arguments[0].MatchLdLocRef(out var variable) && variable.Kind == VariableKind.NamedArgument)
|
|
{
|
|
// remove the store instruction of the simple load, if it is a named argument.
|
|
var storeInst = (ILInstruction)variable.StoreInstructions[0];
|
|
((Block)storeInst.Parent).Instructions.RemoveAt(storeInst.ChildIndex);
|
|
}
|
|
}
|
|
for (int i = 0; i < reducedMethod.NumberOfCompilerGeneratedParameters; i++)
|
|
{
|
|
replacement.AddILRange(useSite.Arguments[argumentCount - i - 1]);
|
|
}
|
|
useSite.ReplaceWith(replacement);
|
|
return replacement;
|
|
}
|
|
|
|
void DetermineCaptureAndDeclarationScope(LocalFunctionInfo info, ILInstruction useSite)
|
|
{
|
|
switch (useSite)
|
|
{
|
|
case CallInstruction call:
|
|
if (DelegateConstruction.MatchDelegateConstruction(useSite, out _, out _, out _))
|
|
{
|
|
// if this is a delegate construction, skip the use-site, because the capture scope
|
|
// was already determined when analyzing "this".
|
|
break;
|
|
}
|
|
int firstArgumentIndex = info.Definition.Method.IsStatic ? 0 : 1;
|
|
for (int i = call.Arguments.Count - 1; i >= firstArgumentIndex; i--)
|
|
{
|
|
if (!DetermineCaptureAndDeclarationScope(info, i - firstArgumentIndex, call.Arguments[i]))
|
|
break;
|
|
}
|
|
if (firstArgumentIndex > 0)
|
|
{
|
|
DetermineCaptureAndDeclarationScope(info, -1, call.Arguments[0]);
|
|
}
|
|
break;
|
|
case LdFtn _:
|
|
// &LocalFunction is only possible, if the local function is declared static,
|
|
// this means that the local function can be declared in the top-level scope.
|
|
// leave info.DeclarationScope null/unassigned.
|
|
break;
|
|
default:
|
|
throw new NotSupportedException();
|
|
}
|
|
}
|
|
|
|
bool DetermineCaptureAndDeclarationScope(LocalFunctionInfo info, int parameterIndex, ILInstruction arg)
|
|
{
|
|
ILFunction function = info.Definition;
|
|
ILVariable closureVar;
|
|
if (parameterIndex >= 0)
|
|
{
|
|
if (!(parameterIndex < function.Method.Parameters.Count
|
|
&& IsClosureParameter(function.Method.Parameters[parameterIndex], resolveContext)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
if (!(arg.MatchLdLoc(out closureVar) || arg.MatchLdLoca(out closureVar)))
|
|
{
|
|
closureVar = ResolveAncestorScopeReference(arg);
|
|
if (closureVar == null)
|
|
return false;
|
|
}
|
|
if (closureVar.Kind == VariableKind.NamedArgument)
|
|
return false;
|
|
if (parameterIndex >= 0 && closureVar.Kind == VariableKind.Parameter)
|
|
{
|
|
// The use-site sits inside another local function that received the display class as
|
|
// a parameter and forwards it. A parameter has no initializer, so the scope the
|
|
// display class was created in has to be recovered from the variable holding it.
|
|
closureVar = FindClosureVariableOfType(closureVar.Type.UnwrapByRef()) ?? closureVar;
|
|
}
|
|
var initializer = GetClosureInitializer(closureVar);
|
|
if (initializer == null)
|
|
return false;
|
|
// determine the capture scope of closureVar and the declaration scope of the function
|
|
var additionalScope = BlockContainer.FindClosestContainer(initializer);
|
|
if (closureVar.CaptureScope == null)
|
|
closureVar.CaptureScope = additionalScope;
|
|
else
|
|
{
|
|
BlockContainer combinedScope = FindCommonAncestorInstruction<BlockContainer>(closureVar.CaptureScope, additionalScope);
|
|
Debug.Assert(combinedScope != null);
|
|
closureVar.CaptureScope = combinedScope;
|
|
}
|
|
if (closureVar.Kind == VariableKind.Local)
|
|
{
|
|
closureVar.Kind = VariableKind.DisplayClassLocal;
|
|
}
|
|
if (function.DeclarationScope == null)
|
|
function.DeclarationScope = closureVar.CaptureScope;
|
|
else if (closureVar.CaptureScope.IsDescendantOf(function.DeclarationScope))
|
|
{
|
|
// The closures captured by one local function are nested in one another: a
|
|
// local function declared inside a lambda still reaches the enclosing method's
|
|
// closure, but not the other way round. Where one capture scope contains the
|
|
// other, the declaration belongs in the inner one; taking the common ancestor
|
|
// would move the function out of the lambda owning the deeper closure and
|
|
// leave the variables captured there out of scope.
|
|
function.DeclarationScope = closureVar.CaptureScope;
|
|
}
|
|
else if (!function.DeclarationScope.IsDescendantOf(closureVar.CaptureScope)
|
|
&& !IsInNestedLocalFunction(function.DeclarationScope, closureVar.CaptureScope.Ancestors.OfType<ILFunction>().First()))
|
|
{
|
|
function.DeclarationScope = FindCommonAncestorInstruction<BlockContainer>(function.DeclarationScope, closureVar.CaptureScope);
|
|
}
|
|
return true;
|
|
|
|
ILInstruction GetClosureInitializer(ILVariable variable)
|
|
{
|
|
var type = variable.Type.UnwrapByRef().GetDefinition();
|
|
if (type == null)
|
|
return null;
|
|
if (variable.Kind == VariableKind.Parameter)
|
|
return null;
|
|
if (type.Kind == TypeKind.Struct)
|
|
return Block.GetContainingStatement(variable.AddressInstructions.OrderBy(i => i.StartILOffset).First());
|
|
else
|
|
return (StLoc)variable.StoreInstructions[0];
|
|
}
|
|
}
|
|
|
|
bool IsInNestedLocalFunction(BlockContainer declarationScope, ILFunction function)
|
|
{
|
|
return TreeTraversal.PreOrder(function, f => f.LocalFunctions).Any(f => declarationScope.IsDescendantOf(f.Body));
|
|
}
|
|
|
|
internal static bool IsLocalFunctionReference(NewObj inst, ILTransformContext context)
|
|
{
|
|
if (inst == null || inst.Arguments.Count != 2 || inst.Method.DeclaringType.Kind != TypeKind.Delegate)
|
|
return false;
|
|
var opCode = inst.Arguments[1].OpCode;
|
|
|
|
return opCode == OpCode.LdFtn
|
|
&& IsLocalFunctionMethod(((IInstructionWithMethodOperand)inst.Arguments[1]).Method, context);
|
|
}
|
|
|
|
public static bool IsLocalFunctionMethod(IMethod method, ILTransformContext context)
|
|
{
|
|
if (method.MetadataToken.IsNil)
|
|
return false;
|
|
return IsLocalFunctionMethod(method.ParentModule.MetadataFile, (MethodDefinitionHandle)method.MetadataToken, context);
|
|
}
|
|
|
|
public static bool IsLocalFunctionMethod(MetadataFile module, MethodDefinitionHandle methodHandle, ILTransformContext context = null)
|
|
{
|
|
if (context != null && context.PEFile != module)
|
|
return false;
|
|
|
|
var metadata = module.Metadata;
|
|
var method = metadata.GetMethodDefinition(methodHandle);
|
|
var declaringType = method.GetDeclaringType();
|
|
|
|
if ((method.Attributes & MethodAttributes.Assembly) == 0)
|
|
return false;
|
|
|
|
if ((method.IsCompilerGenerated(metadata) || declaringType.IsCompilerGenerated(metadata))
|
|
&& ParseLocalFunctionName(metadata.GetString(method.Name), out _, out _))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Obfuscators strip the CompilerGeneratedAttribute and rewrite the
|
|
// "<caller>g__name|x_y" name, but they cannot remove the by-ref display-struct
|
|
// parameter: a compiler-generated struct closure is only ever passed by reference
|
|
// to the local functions that capture it.
|
|
return HasDisplayStructParameter(module, methodHandle);
|
|
}
|
|
|
|
/// <summary>
|
|
/// True if any parameter is a by-ref compiler-generated closure struct of this module.
|
|
/// </summary>
|
|
static bool HasDisplayStructParameter(MetadataFile module, MethodDefinitionHandle methodHandle)
|
|
{
|
|
var metadata = module.Metadata;
|
|
var method = metadata.GetMethodDefinition(methodHandle);
|
|
FindRefStructParameters visitor = new FindRefStructParameters();
|
|
method.DecodeSignature(visitor, default);
|
|
foreach (var h in visitor.RefStructTypes)
|
|
{
|
|
var td = metadata.GetTypeDefinition(h);
|
|
if (td.IsCompilerGenerated(metadata) && td.IsValueType(metadata) && td.HasGeneratedName(metadata))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static bool LocalFunctionNeedsAccessibilityChange(MetadataFile module, MethodDefinitionHandle methodHandle)
|
|
{
|
|
return IsLocalFunctionMethod(module, methodHandle)
|
|
&& HasDisplayStructParameter(module, methodHandle);
|
|
}
|
|
|
|
public static bool IsLocalFunctionDisplayClass(MetadataFile module, TypeDefinitionHandle typeHandle, ILTransformContext context = null)
|
|
{
|
|
if (context != null && context.PEFile != module)
|
|
return false;
|
|
|
|
var metadata = module.Metadata;
|
|
var type = metadata.GetTypeDefinition(typeHandle);
|
|
|
|
if ((type.Attributes & TypeAttributes.VisibilityMask) != TypeAttributes.NestedPrivate)
|
|
return false;
|
|
if (!type.HasGeneratedName(metadata))
|
|
return false;
|
|
|
|
var declaringTypeHandle = type.GetDeclaringType();
|
|
var declaringType = metadata.GetTypeDefinition(declaringTypeHandle);
|
|
|
|
foreach (var method in declaringType.GetMethods())
|
|
{
|
|
if (!IsLocalFunctionMethod(module, method, context))
|
|
continue;
|
|
var md = metadata.GetMethodDefinition(method);
|
|
if (md.DecodeSignature(new FindTypeDecoder(typeHandle, module), default).ParameterTypes.Any())
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Newer Roslyn versions use the format "<callerName>g__functionName|x_y"
|
|
/// Older versions use "<callerName>g__functionNamex_y"
|
|
/// </summary>
|
|
static readonly Regex functionNameRegex = new Regex(@"^<(.*)>g__([^\|]*)\|{0,1}\d+(_\d+)?$", RegexOptions.Compiled);
|
|
|
|
internal static bool ParseLocalFunctionName(string name, out string callerName, out string functionName)
|
|
{
|
|
callerName = null;
|
|
functionName = null;
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return false;
|
|
var match = functionNameRegex.Match(name);
|
|
callerName = match.Groups[1].Value;
|
|
functionName = match.Groups[2].Value;
|
|
return match.Success;
|
|
}
|
|
|
|
class FindRefStructParameters : ISignatureTypeProvider<TypeDefinitionHandle, Unit>
|
|
{
|
|
public readonly List<TypeDefinitionHandle> RefStructTypes = new List<TypeDefinitionHandle>();
|
|
|
|
public TypeDefinitionHandle GetArrayType(TypeDefinitionHandle elementType, ArrayShape shape) => default;
|
|
public TypeDefinitionHandle GetFunctionPointerType(MethodSignature<TypeDefinitionHandle> signature) => default;
|
|
// A closure struct of a generic method or generic declaring type arrives as an instantiation;
|
|
// its definition handle is what identifies the struct. Cross-module generic types still drop out,
|
|
// because GetTypeFromReference already returned nil for them.
|
|
public TypeDefinitionHandle GetGenericInstantiation(TypeDefinitionHandle genericType, ImmutableArray<TypeDefinitionHandle> typeArguments) => genericType;
|
|
public TypeDefinitionHandle GetGenericMethodParameter(Unit genericContext, int index) => default;
|
|
public TypeDefinitionHandle GetGenericTypeParameter(Unit genericContext, int index) => default;
|
|
public TypeDefinitionHandle GetModifiedType(TypeDefinitionHandle modifier, TypeDefinitionHandle unmodifiedType, bool isRequired) => default;
|
|
public TypeDefinitionHandle GetPinnedType(TypeDefinitionHandle elementType) => default;
|
|
public TypeDefinitionHandle GetPointerType(TypeDefinitionHandle elementType) => default;
|
|
public TypeDefinitionHandle GetPrimitiveType(PrimitiveTypeCode typeCode) => default;
|
|
public TypeDefinitionHandle GetSZArrayType(TypeDefinitionHandle elementType) => default;
|
|
|
|
public TypeDefinitionHandle GetByReferenceType(TypeDefinitionHandle elementType)
|
|
{
|
|
if (!elementType.IsNil)
|
|
RefStructTypes.Add(elementType);
|
|
return elementType;
|
|
}
|
|
|
|
public TypeDefinitionHandle GetTypeFromSpecification(MetadataReader reader, Unit genericContext, TypeSpecificationHandle handle, byte rawTypeKind) => default;
|
|
public TypeDefinitionHandle GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) => handle;
|
|
public TypeDefinitionHandle GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) => default;
|
|
}
|
|
}
|
|
}
|