From 7bf102f3b45ac2b2a0b8e01d174f381b74f775ab Mon Sep 17 00:00:00 2001 From: Christoph Wille Date: Sun, 16 Aug 2026 11:16:12 +0200 Subject: [PATCH] Keep the default literal's type when a conversion is unwrapped Making a conversion implicit by unwrapping it hands the operand to a different target type, and a default literal takes its value from that type: "S? x = new S?(default)" holds a value, while "S? x = default" is null. Unwrapping the nullable constructor around a shortened literal therefore turned "S? x = default(S)" into a null nullable. The literal is spelled out again whenever unwrapping moves it to a type other than the one it was shortened from. Converting a using resource to the declared variable type is unconditional now (except when the declaration says "var", which supplies no type): the declaration always spells the type out, so any conversion to it may stay implicit, which is also what shortens default(T) there. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../TestCases/Pretty/AsyncUsing.cs | 2 +- .../TestCases/Pretty/DefaultLiteral.cs | 6 ++++++ ICSharpCode.Decompiler/CSharp/StatementBuilder.cs | 13 ++++++------- .../CSharp/TranslatedExpression.cs | 15 ++++++++++++--- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncUsing.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncUsing.cs index ce7251b06..876ff4173 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncUsing.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AsyncUsing.cs @@ -49,7 +49,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public static async void TestAsyncUsingNullableStruct() { - await using (AsyncDisposableStruct? asyncDisposableStruct = new AsyncDisposableStruct?(default)) + await using (AsyncDisposableStruct? asyncDisposableStruct = default(AsyncDisposableStruct)) { Use(asyncDisposableStruct); } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DefaultLiteral.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DefaultLiteral.cs index 515188dd3..b8a8e5911 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DefaultLiteral.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DefaultLiteral.cs @@ -100,6 +100,12 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty return data + default(Data); } + public bool NullableWithValueStaysTyped() + { + Data? data = default(Data); + return data.HasValue; + } + public string NonIdentityConversionsStayTyped() { object obj = default(Data); diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index 92a28762c..1314702d1 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -543,11 +543,11 @@ namespace ICSharpCode.Decompiler.CSharp protected internal override TranslatedStatement VisitUsingInstruction(UsingInstruction inst) { - var resource = exprBuilder.Translate(inst.ResourceExpression).Expression; + var resource = exprBuilder.Translate(inst.ResourceExpression); var transformed = TransformToForeach(inst, resource); if (transformed != null) return transformed.WithILInstruction(inst); - AstNode usingInit = resource; + AstNode usingInit = resource.Expression; var var = inst.Variable; KnownTypeCode knownTypeCode; IType disposeType; @@ -578,7 +578,7 @@ namespace ICSharpCode.Decompiler.CSharp disposeInvocation = new UnaryOperatorExpression { Expression = disposeInvocation, Operator = UnaryOperatorType.Await }; } return new BlockStatement { - new ExpressionStatement(new AssignmentExpression(exprBuilder.ConvertVariable(var).Expression, resource.Detach())), + new ExpressionStatement(new AssignmentExpression(exprBuilder.ConvertVariable(var).Expression, resource.Expression.Detach())), new TryCatchStatement { TryBlock = ConvertAsBlock(inst.Body), FinallyBlock = new BlockStatement() { @@ -596,12 +596,11 @@ namespace ICSharpCode.Decompiler.CSharp if (var.LoadCount > 0 || var.AddressCount > 0) { var type = settings.AnonymousTypes && var.Type.ContainsAnonymousType() ? new SimpleType("var") : exprBuilder.ConvertType(var.Type); - if (resource is DefaultValueExpression) + if (!type.IsVar()) { // Unlike "using (expr)", the declaration spells out the type, so the - // resource may use the default literal. - resource = new TranslatedExpression(resource) - .ConvertTo(var.Type, exprBuilder, allowImplicitConversion: true); + // resource may leave conversions to it implicit. + resource = resource.ConvertTo(var.Type, exprBuilder, allowImplicitConversion: true); } var vds = new VariableDeclarationStatement(type, var.Name!, resource); vds.Variables.Single().AddAnnotation(new ILVariableResolveResult(var, var.Type)); diff --git a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs index 4d40d998f..d682da2eb 100644 --- a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs @@ -224,6 +224,15 @@ namespace ICSharpCode.Decompiler.CSharp return RestoreDefaultLiteralType(expressionBuilder) .ConvertTo(targetType, expressionBuilder, checkForOverflow, allowImplicitConversion); } + // Unwrapping a conversion hands its operand to a different target type. A default + // literal takes its value from that type, so it may have to be spelled out again: + // "T? x = new T?(default)" holds a value, whereas "T? x = default" is null. + TranslatedExpression Unwrapped(TranslatedExpression operand) + { + if (operand.ResolveResult is not DefaultLiteralResolveResult) + return operand; + return operand.ConvertTo(targetType, expressionBuilder, checkForOverflow, allowImplicitConversion); + } if (NormalizeTypeVisitor.IgnoreNullabilityAndTuples.EquivalentTypes(type, targetType)) { // Make explicit conversion implicit, if possible @@ -251,7 +260,7 @@ namespace ICSharpCode.Decompiler.CSharp type, targetType )) { - var result = this.UnwrapChild(cast.Expression); + var result = Unwrapped(this.UnwrapChild(cast.Expression)); if (conversion.Conversion.IsUserDefined) { result.Expression.AddAnnotation(new ImplicitConversionAnnotation(conversion)); @@ -270,7 +279,7 @@ namespace ICSharpCode.Decompiler.CSharp if (Expression is ObjectCreateExpression oce && oce.Arguments.Count == 1 && invocation.Type.IsKnownType(KnownTypeCode.NullableOfT)) { - return this.UnwrapChild(oce.Arguments.Single()); + return Unwrapped(this.UnwrapChild(oce.Arguments.Single())); } break; } @@ -342,7 +351,7 @@ namespace ICSharpCode.Decompiler.CSharp && !conv.Conversion.IsUserDefined && CastCanBeMadeImplicit(conversions, conv.Conversion, conv.Input.Type, type, targetType)) { - var unwrapped = this.UnwrapChild(cast2.Expression); + var unwrapped = Unwrapped(this.UnwrapChild(cast2.Expression)); if (allowImplicitConversion) return unwrapped; return unwrapped.ConvertTo(targetType, expressionBuilder, checkForOverflow, allowImplicitConversion);