From 40b90766a11cf63cbd9bbc6c1c85f22e4ac43055 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 29 Aug 2026 08:47:49 +0200 Subject: [PATCH] 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 --- .../TestCases/Pretty/DynamicTests.cs | 60 ++----------------- .../TestCases/Pretty/NullPropagation.cs | 11 ++++ .../CSharp/ExpressionBuilder.cs | 6 +- ICSharpCode.Decompiler/IL/ILTypeExtensions.cs | 6 ++ 4 files changed, 24 insertions(+), 59 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs index 9fb7bc2b3..8ecde801a 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs @@ -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 { } - private static dynamic M5(dynamic d, int i) - { - return null; - } - - private static void M6(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(d, (data != null) ? data.Length : 0); -#else - DynamicTests.M6(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 diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs index c630c5d4f..b2c149034 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs @@ -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]; diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index ec1be0aa4..f89ce6f99 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -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) { diff --git a/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs b/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs index 459cfef76..ca8da349a 100644 --- a/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs +++ b/ICSharpCode.Decompiler/IL/ILTypeExtensions.cs @@ -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: