Browse Source

Let an array initializer block evaluate to a Span<T>

The Span<T>/ReadOnlySpan<T> initializer patterns replaced their call with an
array initializer block, so an array stood where a span was expected: the
enclosing leave and any call taking the result saw StackType.Obj against the
StackType.VT the span type demands. The block now ends in the implicit
conversion the C# compiler applies, which is the one shape besides a bare
ldloc that an array initializer may take; the expression builder keeps the
conversion out of the output but not out of the expression's type.

Naming that operator wants a method looked up by signature rather than by a
predicate over the type's members, so MetadataModule grows a ResolveMethod
overload for it, sharing its signature matching with the metadata path.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/4091/head
Siegfried Pammer 2 weeks ago committed by Daniel Grunwald
parent
commit
3a3a79e88f
  1. 16
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  2. 22
      ICSharpCode.Decompiler/IL/Instructions/Block.cs
  3. 40
      ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs
  4. 68
      ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs

16
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -3880,7 +3880,7 @@ namespace ICSharpCode.Decompiler.CSharp
TranslatedExpression TranslateArrayInitializer(Block block) TranslatedExpression TranslateArrayInitializer(Block block)
{ {
var stloc = block.Instructions.FirstOrDefault() as StLoc; var stloc = block.Instructions.FirstOrDefault() as StLoc;
var final = block.FinalInstruction as LdLoc; var final = Block.MatchArrayInitializerFinal(block.FinalInstruction, out var arrayToSpan);
if (stloc == null || final == null || !stloc.Value.MatchNewArr(out IType? type)) if (stloc == null || final == null || !stloc.Value.MatchNewArr(out IType? type))
throw new ArgumentException("given Block is invalid!"); throw new ArgumentException("given Block is invalid!");
if (stloc.Variable != final.Variable || stloc.Variable.Kind != VariableKind.InitializerTarget) if (stloc.Variable != final.Variable || stloc.Variable.Kind != VariableKind.InitializerTarget)
@ -3961,8 +3961,18 @@ namespace ICSharpCode.Decompiler.CSharp
expr.AdditionalArraySpecifiers.AddRange(additionalSpecifiers); expr.AdditionalArraySpecifiers.AddRange(additionalSpecifiers);
if (!type.ContainsAnonymousType()) if (!type.ContainsAnonymousType())
expr.Arguments.AddRange(newArr.Indices.Select(i => Translate(i).Expression)); expr.Arguments.AddRange(newArr.Indices.Select(i => Translate(i).Expression));
return expr.WithILInstruction(block) ResolveResult rr = new ArrayCreateResolveResult(new ArrayType(compilation, type, dimensions),
.WithRR(new ArrayCreateResolveResult(new ArrayType(compilation, type, dimensions), newArr.Indices.Select(i => Translate(i).ResolveResult).ToArray(), elementResolveResults)); newArr.Indices.Select(i => Translate(i).ResolveResult).ToArray(), elementResolveResults);
if (arrayToSpan != null)
{
// The block converts its array to Span<T>/ReadOnlySpan<T>. The conversion is
// implicit in C#, so the array initializer stands on its own, but the expression
// still has the span type: the block evaluates to a span, not to an array.
rr = new ConversionResolveResult(arrayToSpan.ReturnType, rr,
Conversion.UserDefinedConversion(arrayToSpan, isImplicit: true,
Conversion.IdentityConversion, Conversion.IdentityConversion));
}
return expr.WithILInstruction(block).WithRR(rr);
} }
TranslatedExpression TranslateStackAllocInitializer(Block block, IType typeHint) TranslatedExpression TranslateStackAllocInitializer(Block block, IType typeHint)

22
ICSharpCode.Decompiler/IL/Instructions/Block.cs

@ -103,6 +103,26 @@ namespace ICSharpCode.Decompiler.IL
return clone; return clone;
} }
/// <summary>
/// The load of the initializer target an ArrayInitializer block evaluates to. The block
/// may end in the implicit conversion of that array to Span&lt;T&gt;/ReadOnlySpan&lt;T&gt;,
/// which is returned in <paramref name="conversion"/>; everything else about the block is
/// the same in both shapes.
/// </summary>
internal static LdLoc? MatchArrayInitializerFinal(ILInstruction finalInstruction, out IMethod? conversion)
{
conversion = null;
if (finalInstruction is CallInstruction { Arguments.Count: 1 } call
&& call.Method.IsOperator && call.Method.Name == "op_Implicit"
&& (call.Method.ReturnType.IsKnownType(KnownTypeCode.SpanOfT)
|| call.Method.ReturnType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)))
{
conversion = call.Method;
finalInstruction = call.Arguments[0];
}
return finalInstruction as LdLoc;
}
internal override void CheckInvariant(ILPhase phase, ICompilation compilation) internal override void CheckInvariant(ILPhase phase, ICompilation compilation)
{ {
base.CheckInvariant(phase, compilation); base.CheckInvariant(phase, compilation);
@ -138,7 +158,7 @@ namespace ICSharpCode.Decompiler.IL
} }
break; break;
case BlockKind.ArrayInitializer: case BlockKind.ArrayInitializer:
var final = finalInstruction as LdLoc; var final = MatchArrayInitializerFinal(finalInstruction, out _);
Debug.Assert(final != null && final.Variable.IsSingleDefinition && final.Variable.Kind == VariableKind.InitializerTarget); Debug.Assert(final != null && final.Variable.IsSingleDefinition && final.Variable.Kind == VariableKind.InitializerTarget);
IType? type = null; IType? type = null;
Debug.Assert(Instructions[0].MatchStLoc(final!.Variable, out var init) && init.MatchNewArr(out type)); Debug.Assert(Instructions[0].MatchStLoc(final!.Variable, out var init) && init.MatchNewArr(out type));

40
ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs

@ -19,6 +19,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Collections.Immutable;
using System.Reflection.Metadata; using System.Reflection.Metadata;
using System.Text; using System.Text;
@ -68,7 +69,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
{ {
context.Step("HandleRuntimeHelperInitializeArray: single-dim", inst); context.Step("HandleRuntimeHelperInitializeArray: single-dim", inst);
var tempStore = context.Function.RegisterVariable(VariableKind.InitializerTarget, v.Type); var tempStore = context.Function.RegisterVariable(VariableKind.InitializerTarget, v.Type);
var block = BlockFromInitializer(tempStore, elementType, arrayLength, values); var block = BlockFromInitializer(tempStore, elementType, null, arrayLength, values);
var newStore = new StLoc(v, block); var newStore = new StLoc(v, block);
body.Instructions[pos] = newStore; body.Instructions[pos] = newStore;
body.Instructions.RemoveAt(initArrayPos); body.Instructions.RemoveAt(initArrayPos);
@ -160,24 +161,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (DecodeArrayInitializer(elementType, initialValue, new[] { size }, valuesList)) if (DecodeArrayInitializer(elementType, initialValue, new[] { size }, valuesList))
{ {
var tempStore = context.Function.RegisterVariable(VariableKind.InitializerTarget, new ArrayType(context.TypeSystem, elementType)); var tempStore = context.Function.RegisterVariable(VariableKind.InitializerTarget, new ArrayType(context.TypeSystem, elementType));
ILInstruction result = BlockFromInitializer(tempStore, elementType, new[] { size }, valuesList.ToArray()); IMethod op_Implicit = null;
if (targetType.IsKnownType(KnownTypeCode.SpanOfT) || targetType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)) if (targetType.IsKnownType(KnownTypeCode.SpanOfT) || targetType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
{ {
// The block builds an array where a Span<T>/ReadOnlySpan<T> is expected, so it // The block builds an array where a Span<T>/ReadOnlySpan<T> is expected, so it
// needs the conversion the C# compiler would have applied. It has to wrap the // ends in the conversion the C# compiler would have applied.
// block: an ArrayInitializer block must keep ldloc as its final instruction. op_Implicit = context.TypeSystem.MainModule.ResolveMethod(targetType, "op_Implicit",
var op_Implicit = targetType.GetMethods(m => m.IsOperator && m.Name == "op_Implicit" new MethodSignature<IType>(
&& m.Parameters.Count == 1 new SignatureHeader(SignatureKind.Method, SignatureCallingConvention.Default, SignatureAttributes.None),
&& m.Parameters[0].Type.Kind == TypeKind.Array).FirstOrDefault(); returnType: targetType,
if (op_Implicit == null) requiredParameterCount: 1,
{ genericParameterCount: 0,
// Without the operator the conversion cannot be expressed; leave the parameterTypes: ImmutableArray.Create<IType>(new ArrayType(context.TypeSystem, elementType))));
// original call alone rather than produce an array where a span belongs.
return null;
}
result = new Call(op_Implicit) { Arguments = { result } };
} }
return result; return BlockFromInitializer(tempStore, elementType, op_Implicit, new[] { size }, valuesList.ToArray());
} }
return null; return null;
@ -260,7 +257,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (HandleRuntimeHelpersInitializeArray(body, pos + 1, v, elementType, length, out var values, out var initArrayPos)) if (HandleRuntimeHelpersInitializeArray(body, pos + 1, v, elementType, length, out var values, out var initArrayPos))
{ {
context.Step("HandleRuntimeHelpersInitializeArray: multi-dim", inst); context.Step("HandleRuntimeHelpersInitializeArray: multi-dim", inst);
var block = BlockFromInitializer(v, elementType, length, values); var block = BlockFromInitializer(v, elementType, null, length, values);
var newStore = new StLoc(v, block); var newStore = new StLoc(v, block);
body.Instructions[pos].ReplaceWith(newStore); body.Instructions[pos].ReplaceWith(newStore);
body.Instructions.RemoveAt(initArrayPos); body.Instructions.RemoveAt(initArrayPos);
@ -837,7 +834,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
&& initializer.OpCode == OpCode.Block; && initializer.OpCode == OpCode.Block;
} }
static Block BlockFromInitializer(ILVariable v, IType elementType, int[] arrayLength, ILInstruction[] values) static Block BlockFromInitializer(ILVariable v, IType elementType, IMethod arrayToSpan, int[] arrayLength, ILInstruction[] values)
{ {
var block = new Block(BlockKind.ArrayInitializer); var block = new Block(BlockKind.ArrayInitializer);
block.Instructions.Add(new StLoc(v, new NewArr(elementType, arrayLength.Select(l => new LdcI4(l)).ToArray()))); block.Instructions.Add(new StLoc(v, new NewArr(elementType, arrayLength.Select(l => new LdcI4(l)).ToArray())));
@ -858,7 +855,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
block.Instructions.Add(StElem(new LdLoc(v), indices.ToArray(), value, elementType)); block.Instructions.Add(StElem(new LdLoc(v), indices.ToArray(), value, elementType));
indices.Clear(); indices.Clear();
} }
block.FinalInstruction = new LdLoc(v); ILInstruction final = new LdLoc(v);
if (arrayToSpan != null)
{
final = new Call(arrayToSpan) { Arguments = { final } };
}
block.FinalInstruction = final;
return block; return block;
} }
@ -966,7 +968,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false; return false;
context.Step("InlineRuntimeHelpersInitializeArray: single-dim", inst); context.Step("InlineRuntimeHelpersInitializeArray: single-dim", inst);
var tempStore = context.Function.RegisterVariable(VariableKind.InitializerTarget, new ArrayType(context.TypeSystem, elementType, arrayLength.Length)); var tempStore = context.Function.RegisterVariable(VariableKind.InitializerTarget, new ArrayType(context.TypeSystem, elementType, arrayLength.Length));
var block = BlockFromInitializer(tempStore, elementType, arrayLength, valuesList.ToArray()); var block = BlockFromInitializer(tempStore, elementType, null, arrayLength, valuesList.ToArray());
body.Instructions[pos] = block; body.Instructions[pos] = block;
context.EndStep(block); context.EndStep(block);
ILInlining.InlineIfPossible(body, pos, context); ILInlining.InlineIfPossible(body, pos, context);

68
ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs

@ -568,19 +568,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
parameterTypes = signature.ParameterTypes; parameterTypes = signature.ParameterTypes;
} }
// Search for the matching method: // Search for the matching method:
method = null; method = FindMethod(methods, signature, parameterTypes);
foreach (var m in methods)
{
if (m.TypeParameters.Count != signature.GenericParameterCount)
continue;
if (signature.Header.IsInstance != !m.IsStatic)
continue;
if (CompareSignatures(m.Parameters, parameterTypes) && CompareTypes(m.ReturnType, signature.ReturnType))
{
method = m;
break;
}
}
} }
else else
{ {
@ -602,6 +590,60 @@ namespace ICSharpCode.Decompiler.TypeSystem
return method; return method;
} }
/// <summary>
/// Resolves a method on <paramref name="declaringType"/> by name and signature.
/// If the type declares no such method - because the reference is missing, or the
/// method does not exist on the version at hand - a fake method carrying the requested
/// signature is returned, as for a method reference that cannot be resolved.
/// </summary>
/// <remarks>
/// The signature is matched against the members of <paramref name="declaringType"/> as
/// they are seen from the outside, so for a parameterized type it is written in terms of
/// the type arguments, not the type parameters. This is the lookup a decompiler step
/// needs when it has to name a specific method - a conversion operator, say - rather
/// than one it read from metadata.
/// </remarks>
public IMethod ResolveMethod(IType declaringType, string name, MethodSignature<IType> signature)
{
if (declaringType == null)
throw new ArgumentNullException(nameof(declaringType));
if (name == null)
throw new ArgumentNullException(nameof(name));
IEnumerable<IMethod> methods;
if (name == ".ctor")
{
methods = declaringType.GetConstructors();
}
else
{
methods = declaringType.GetMethods(m => m.Name == name)
.Concat(declaringType.GetAccessors(m => m.Name == name));
}
return FindMethod(methods, signature, signature.ParameterTypes)
?? CreateFakeMethod(declaringType, name, signature);
}
/// <summary>
/// The single method among <paramref name="candidates"/> that matches the signature, or
/// null. <paramref name="parameterTypes"/> is passed separately because a vararg
/// signature is matched against its required parameters plus __arglist.
/// </summary>
static IMethod FindMethod(IEnumerable<IMethod> candidates, MethodSignature<IType> signature, ImmutableArray<IType> parameterTypes)
{
foreach (var method in candidates)
{
if (method.TypeParameters.Count != signature.GenericParameterCount)
continue;
if (signature.Header.IsInstance != !method.IsStatic)
continue;
if (CompareSignatures(method.Parameters, parameterTypes) && CompareTypes(method.ReturnType, signature.ReturnType))
{
return method;
}
}
return null;
}
static readonly NormalizeTypeVisitor normalizeTypeVisitor = new NormalizeTypeVisitor { static readonly NormalizeTypeVisitor normalizeTypeVisitor = new NormalizeTypeVisitor {
ReplaceClassTypeParametersWithDummy = true, ReplaceClassTypeParametersWithDummy = true,
ReplaceMethodTypeParametersWithDummy = true, ReplaceMethodTypeParametersWithDummy = true,

Loading…
Cancel
Save