Browse Source

Infer the C# type of ldlen

NullPropagationTransform only rewrites "x != null ? x.Chain : fallback"
into "x?.Chain ?? fallback" when the chain's inferred type is a
non-nullable value type, and InferType had no case for ldlen. Array length
therefore came back as UnknownType, so "arr?.Length ?? 0" was left as a
ternary.

The inferred type mirrors ExpressionBuilder.VisitLdLen, which decides
between Array.Length and Array.LongLength from the result type alone.

Found while investigating #3704, where the surviving ternary also keeps the
tested array in a stack slot and strands the typeof of a dynamic call's
static target. That issue is fixed separately in #4072, whose DynamicTests
cases pinned the ternary as expected output; those blocks round-trip
exactly now, so they are gone.

Also carries a review follow-up that missed #4072: the static-target test
in VisitDynamicInvokeMemberInstruction is a plain null check, the way
DynamicInvokeMemberInstruction itself tests the field, rather than a
pattern match binding a name it does not need.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
pull/4075/head
Siegfried Pammer 3 weeks ago
parent
commit
40b90766a1
  1. 60
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs
  2. 11
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs
  3. 6
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  4. 6
      ICSharpCode.Decompiler/IL/ILTypeExtensions.cs

60
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs

@ -23,13 +23,6 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -23,13 +23,6 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
}
private class CtorTarget
{
public CtorTarget(dynamic d, int i)
{
}
}
private struct MyValueType
{
private readonly dynamic _getOnlyProperty;
@ -384,57 +377,12 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -384,57 +377,12 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
}
private static dynamic M5(dynamic d, int i)
{
return null;
}
private static void M6<T>(dynamic d, int i)
{
}
#if CS60
// #3704: the call target of a dynamic member access on a static type is typeof(DynamicTests).
// The array is loaded into a temporary that survives as its own statement, which strands the
// typeof between the two, so it cannot reach the call site by ordinary inlining.
private static void StaticTargetBehindSurvivingTemporary(dynamic d)
{
#if EXPECTED_OUTPUT
byte[] data = GetData();
DynamicTests.M4(d, (data != null) ? data.Length : 0);
#else
DynamicTests.M4(d, GetData()?.Length ?? 0);
#endif
}
private static dynamic StaticTargetResultUsed(dynamic d)
{
#if EXPECTED_OUTPUT
byte[] data = GetData();
return DynamicTests.M5(d, (data != null) ? data.Length : 0);
#else
return DynamicTests.M5(d, GetData()?.Length ?? 0);
#endif
}
private static void StaticTargetWithTypeArguments(dynamic d)
// #3704: the call target of a dynamic member access on a static type is
// typeof(DynamicTests), which has to reach the call site.
private static void StaticTargetOnDynamicCall(dynamic d)
{
#if EXPECTED_OUTPUT
byte[] data = GetData();
DynamicTests.M6<int>(d, (data != null) ? data.Length : 0);
#else
DynamicTests.M6<int>(d, GetData()?.Length ?? 0);
#endif
}
private static void StaticCtorBehindSurvivingTemporary(dynamic d)
{
#if EXPECTED_OUTPUT
byte[] data = GetData();
new CtorTarget(d, (data != null) ? data.Length : 0);
#else
new CtorTarget(d, GetData()?.Length ?? 0);
#endif
DynamicTests.M4(d, GetData()?.Length ?? 0);
}
#endif

11
ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs

@ -320,6 +320,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty @@ -320,6 +320,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
Console.WriteLine(setsOfNumbers?[1]?[1].ToString() == null);
}
private static byte[] GetBytes()
{
return null;
}
private static void ArrayLengthWithFallback()
{
Console.WriteLine(GetBytes()?.Length ?? 0);
Console.WriteLine(GetBytes()?.LongLength ?? 0);
}
private static dynamic DynamicNullProp(dynamic a)
{
return a?.b.c(1)?.d[10];

6
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -4584,10 +4584,10 @@ namespace ICSharpCode.Decompiler.CSharp @@ -4584,10 +4584,10 @@ namespace ICSharpCode.Decompiler.CSharp
protected internal override TranslatedExpression VisitDynamicInvokeMemberInstruction(DynamicInvokeMemberInstruction inst, TranslationContext context)
{
Expression targetExpr;
var target = inst.StaticTargetType is IType staticTargetType
? new TypeReferenceExpression(ConvertType(staticTargetType))
var target = inst.StaticTargetType != null
? new TypeReferenceExpression(ConvertType(inst.StaticTargetType))
.WithoutILInstruction()
.WithRR(new TypeResolveResult(staticTargetType))
.WithRR(new TypeResolveResult(inst.StaticTargetType))
: TranslateDynamicTarget(inst.Arguments[0], inst.ArgumentInfo[0]);
if (inst.BinderFlags.HasFlag(CSharpBinderFlags.InvokeSimpleName) && target.Expression is ThisReferenceExpression)
{

6
ICSharpCode.Decompiler/IL/ILTypeExtensions.cs

@ -287,6 +287,12 @@ namespace ICSharpCode.Decompiler.IL @@ -287,6 +287,12 @@ namespace ICSharpCode.Decompiler.IL
default:
return SpecialType.UnknownType;
}
case LdLen ldLen:
if (compilation == null)
return SpecialType.UnknownType;
// Mirrors ExpressionBuilder.VisitLdLen, which picks Array.Length or
// Array.LongLength based on the result type alone.
return compilation.FindType(ldLen.ResultType == StackType.I4 ? KnownTypeCode.Int32 : KnownTypeCode.Int64);
case DefaultValue defaultValue:
return defaultValue.Type;
case ILFunction func when func.DelegateType != null:

Loading…
Cancel
Save