Skip to content
Merged
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
24 changes: 24 additions & 0 deletions src/ILInspector.Decompiler.Tests/ControlFlowGraphTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,30 @@ public static int RefKindCallSites(int a)
InHelper(in r);
return r + o;
}

// Calls on a constructed generic instance resolve as MemberReferences
// (TypeSpec parent) that carry no parameter rows, so the out/in keyword must
// be recovered from the underlying generic MethodDef, not the MemberRef.
public static int GenericRefKindCallSites(int a)
{
var box = new RefKindBox<int>();
box.TryGet(out int value);
box.Put(in a);
return value;
}
}

public sealed class RefKindBox<T>
{
T _value = default!;

public bool TryGet(out T value)
{
value = _value;
return true;
}

public void Put(in T value) => _value = value;
}

public interface IJoinShape
Expand Down
16 changes: 16 additions & 0 deletions src/ILInspector.Decompiler.Tests/IrImporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,22 @@ public void CallSite_RefKinds_PrintRefOutAndBareIn()
Assert.DoesNotContain("InHelper(out", output);
}

[Fact]
public void CallSite_RefKinds_RecoveredForGenericInstanceCalls()
{
// A call on a constructed generic type is a MemberRef (TypeSpec parent)
// with no parameter rows; the keyword is recovered from the underlying
// generic MethodDef. Without that, `out` would render as `ref` (CS1620).
using var source = MetadataSource.Open(typeof(CfgSampleClass).Assembly.Location);
string output = PrintWithPasses(typeof(CfgSampleClass).FullName!, nameof(CfgSampleClass.GenericRefKindCallSites), source);

Assert.Contains("TryGet(out ", output);
// The `in` argument is passed without a keyword.
Assert.Contains("Put(", output);
Assert.DoesNotContain("Put(ref", output);
Assert.DoesNotContain("Put(out", output);
}

[Fact]
public void BooleanMaterialization_SelectWithIntLiteral_DeclaresBoolSlot()
{
Expand Down
27 changes: 20 additions & 7 deletions src/ILInspector.Decompiler/Pipeline/Ir/CSharpPrinter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1147,14 +1147,15 @@ string Arguments(IEnumerable<IrExpression> arguments, IReadOnlyList<TypeRef> par
{
if (parameter is not { Kind: TypeRefKind.ByRef } || refKind == ArgumentRefKind.Value)
return null;
if (ArgumentPlace(argument) is not { } place)
// `in` accepts a value argument (the compiler introduces a temporary), so
// any place- or value-spelling works and the keyword stays implicit.
if (refKind == ArgumentRefKind.In)
return ArgumentPlace(argument);
// `out`/`ref` require a genuine assignable lvalue; a cast (unbox) is not
// one (`out (T)x` is CS0206), so leave those to the default spelling.
if (ArgumentLvalue(argument) is not { } place)
return null;
return refKind switch
{
ArgumentRefKind.Out => $"out {place}",
ArgumentRefKind.In => place,
_ => $"ref {place}",
};
return refKind == ArgumentRefKind.Out ? $"out {place}" : $"ref {place}";
}

/// <summary>
Expand All @@ -1172,6 +1173,18 @@ string Arguments(IEnumerable<IrExpression> arguments, IReadOnlyList<TypeRef> par
_ => null,
};

/// <summary>
/// The subset of <see cref="ArgumentPlace"/> that is a genuine assignable
/// lvalue — what <c>out</c>/<c>ref</c> demand. Excludes the <see cref="Unbox"/>
/// cast form (an lvalue only `in` can accept, as a value).
/// </summary>
string? ArgumentLvalue(IrExpression argument) => argument switch
{
LoadLocalAddress or LoadArgumentAddress or LoadFieldAddress or LoadElementAddress => Deref(argument),
LoadLocal or LoadArgument or LoadIndirect or Call or CallIndirect => Expression(argument),
_ => null,
};

/// <summary>
/// <c>target?.Member</c>: the member's receiver child is the target, and the
/// member's name/arguments form the suffix after <c>?</c>. Mirrors the
Expand Down
70 changes: 67 additions & 3 deletions src/ILInspector.Decompiler/Pipeline/Ir/IrImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,10 @@ [.. signature.ParameterTypes.Select(p => p.Instantiate(typeArguments, []))],
|| memberName.StartsWith("set_", StringComparison.Ordinal)
|| memberName.StartsWith("op_", StringComparison.Ordinal)
|| memberName is ".ctor" or ".cctor",
// A same-assembly call on a generic type instance is a
// MemberRef (TypeSpec parent), so its ref/out/in would
// otherwise be lost; recover it from the underlying MethodDef.
ParameterRefKinds = MemberReferenceRefKinds(reader, member, memberName, signature.ParameterTypes),
};
}
case HandleKind.MethodSpecification:
Expand Down Expand Up @@ -1296,9 +1300,69 @@ static ImmutableArray<ArgumentRefKind> ReadParameterRefKinds(MetadataReader read
return ImmutableArray.Create(kinds);
}

/// <summary>
/// Recovers ref/out/in for a callee referenced as a MemberReference by
/// resolving it back to its MethodDef parameter rows — the case that matters
/// is a same-assembly call on a generic type instance (TypeSpec parent),
/// where the keyword would otherwise be dropped. Returns empty (the printer
/// keeps its default spelling) when the declaring type is not a same-assembly
/// definition or no signature-matching method is found.
/// </summary>
static ImmutableArray<ArgumentRefKind> MemberReferenceRefKinds(MetadataReader reader, MemberReference member, string memberName, ImmutableArray<TypeRef> parameterTypes)
{
bool anyByRef = false;
foreach (var p in parameterTypes)
if (p.Kind == TypeRefKind.ByRef) { anyByRef = true; break; }
if (!anyByRef || DeclaringTypeDefinition(reader, member.Parent) is not { } typeHandle)
return [];

var memberSignature = reader.GetBlobBytes(member.Signature);
foreach (var methodHandle in reader.GetTypeDefinition(typeHandle).GetMethods())
{
var method = reader.GetMethodDefinition(methodHandle);
if (reader.GetString(method.Name) != memberName)
continue;
// A MemberRef's signature is encoded in the same generic-definition
// terms (!0/!1) as the MethodDef's, so the blobs match byte-for-byte
// for the referenced method — a robust overload-safe match.
if (reader.GetBlobBytes(method.Signature).AsSpan().SequenceEqual(memberSignature))
return ReadParameterRefKinds(reader, method, parameterTypes);
}
return [];
}

/// <summary>
/// The same-assembly <see cref="TypeDefinitionHandle"/> a MemberRef's parent
/// names: the parent directly, or the generic type definition behind a
/// <c>GENERICINST</c> TypeSpecification. Null for a TypeReference (the type is
/// in another assembly, so its parameter rows are not available here).
/// </summary>
static TypeDefinitionHandle? DeclaringTypeDefinition(MetadataReader reader, EntityHandle parent)
{
switch (parent.Kind)
{
case HandleKind.TypeDefinition:
return (TypeDefinitionHandle)parent;
case HandleKind.TypeSpecification:
var blob = reader.GetBlobReader(reader.GetTypeSpecification((TypeSpecificationHandle)parent).Signature);
if (blob.ReadByte() != (byte)SignatureTypeCode.GenericTypeInstance)
return null;
blob.ReadByte(); // ELEMENT_TYPE_CLASS / ELEMENT_TYPE_VALUETYPE
var underlying = blob.ReadTypeHandle();
return underlying.Kind == HandleKind.TypeDefinition ? (TypeDefinitionHandle)underlying : null;
default:
return null;
}
}

static ArgumentRefKind ClassifyByRefParameter(MetadataReader reader, System.Reflection.Metadata.Parameter parameter)
{
if (HasReadOnlyAttribute(reader, parameter.GetCustomAttributes()))
// `in` (IsReadOnlyAttribute) and `ref readonly` (RequiresLocationAttribute)
// both take a readonly reference; the call site spells them without a
// mutable-ref keyword (treated as In — rendered bare), which is valid for
// a readonly or rvalue argument. Both also set the raw In flag (as do
// interop-marshalled `ref` params), so the flag cannot detect them.
if (HasReadOnlyRefAttribute(reader, parameter.GetCustomAttributes()))
return ArgumentRefKind.In;
var attributes = parameter.Attributes;
if ((attributes & System.Reflection.ParameterAttributes.Out) != 0
Expand All @@ -1309,11 +1373,11 @@ static ArgumentRefKind ClassifyByRefParameter(MetadataReader reader, System.Refl

// Pure-SRM attribute presence check (the decompiler Pipeline stays SRM-only,
// so it does not reach into the Metadata AttributeReader).
static bool HasReadOnlyAttribute(MetadataReader reader, CustomAttributeHandleCollection attributes)
static bool HasReadOnlyRefAttribute(MetadataReader reader, CustomAttributeHandleCollection attributes)
{
foreach (var handle in attributes)
if (AttributeTypeName(reader, reader.GetCustomAttribute(handle).Constructor)
is ("System.Runtime.CompilerServices", "IsReadOnlyAttribute"))
is ("System.Runtime.CompilerServices", "IsReadOnlyAttribute" or "RequiresLocationAttribute"))
return true;
return false;
}
Expand Down
Loading