Skip to content

Conversation

@RikkiGibson
Copy link
Member

@RikkiGibson RikkiGibson commented Aug 20, 2025

Closes #78932

#78932 (comment)

What's happening is: in some cases we do a "child" nullable analysis of a lambda to figure out its return type. If we analyze a lambda which uses the field keyword, we will likely ask for its inferred nullable annotation, which launches another analysis of the get accessor, causing us to analyze the lambda return type again. We keep calling thru this cycle until we blow the stack.

What we need to do to fix it is, pass down any context around the getter null resilience analysis that may be currently taking place. In which case we can avoid kicking off a new inference of the backing field's nullable annotation, and avoid getting into a cycle.


One alternative here might be to include GetterNullResilienceData in VariableState itself. VariableState seems to be used to propagate flow state down to analysis of lambdas and local functions, as well as propagating state from "field/property initializers analysis" to constructor analysis.

This state is not really flow state, but it's close.. the nullable annotation for the field determines the field's initial state, and we came pretty close to spec'ing this feature in terms of what is the initial flow state of the field rather than what is its "inferred annotation".

@RikkiGibson RikkiGibson changed the title Break cycle for lambda using field keyword whose return type inferred Break cycle for lambda using field keyword with inferred return type Aug 20, 2025
}
}

internal readonly struct GetterNullResilienceData(SynthesizedBackingFieldSymbol field, NullableAnnotation assumedAnnotation)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty much the same as the tuple we had before but it felt like as the usage was starting to leak into more places, it was better off as a named type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider documenting this type. what is represents (including examples) when when it would be used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as examples, i don't know what 'assumed' means in this context at all.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating docs and updated spec PR dotnet/csharplang#9184

@RikkiGibson RikkiGibson marked this pull request as ready for review August 21, 2025 23:23
@RikkiGibson RikkiGibson requested a review from a team as a code owner August 21, 2025 23:23
@CyrusNajmabadi
Copy link
Member

This state is not really flow state, but it's close..

One thing tha twould be helpful in the OP is to state if this follows the spec in terms of correctness, or if we're adding some sort of heuristic to get out of a bad situation, but it means we don't align with the lang. It's unclear to me from your explanation which path we're on (or something else entirely). Thanks!

/// only needed when nullability is inferred.
/// </summary>
public TypeWithAnnotations GetInferredReturnType(ConversionsBase? conversions, NullableWalker.VariableState? nullableState, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo, out bool inferredFromFunctionType)
public TypeWithAnnotations GetInferredReturnType(ConversionsBase? conversions, NullableWalker.VariableState? nullableState, NullableWalker.GetterNullResilienceData? getterNullResilienceData, ref CompoundUseSiteInfo<AssemblySymbol> useSiteInfo, out bool inferredFromFunctionType)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider updating the docs here. To me, the name GetterNullResilienceData getterNullResilienceData isn't immediately apparent as to what purpose it serves or when it would be used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating.


private T MethodReturningLambda<T>(Func<T> thisGet) => thisGet();
}
""";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for indenting.

SyntaxNode syntax,
DiagnosticBag diagnostics,
(SourcePropertyAccessorSymbol getter, SynthesizedBackingFieldSymbol field, NullableAnnotation assumedNullableAnnotation)? getterNullResilienceData = null)
(SourcePropertyAccessorSymbol symbol, GetterNullResilienceData getterNullResilienceData)? symbolAndGetterNullResilienceData = null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated to this particular change, but it does seem sa bit odd to me to have all this data just stashed as an optional variable at the end of this (seemingly) major entrypoint into nullable walking.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usages of this overload are somewhat specialized and include:

  • attributes
  • parameter default values
  • computing the backing field inferred nullability

Only in the latter case is a value actually passed, so that we can indicate that we are trying to do the "not-annotated pass" versus the "annotated" pass, so that caller can decide if the associated property is null-resilient.

Copy link
Member Author

@RikkiGibson RikkiGibson Aug 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically the backing field case could have its own overload or call one of the more general overloads directly, it just didn't strike me as a valuable change, it works well enough to have a parameter that only one caller passes a value for.

@CyrusNajmabadi
Copy link
Member

So overall this change seems simple enough. but i'm not truly understanding how the 'cycle' is broken. Could you make a note in the PR where that happens?

Note: this feedback applies to prs in general. An occasional comment explaining the supporting pieces, and then how it comes together can be very helpful. Thanks!

@RikkiGibson
Copy link
Member Author

Have updated PR description with more clear information about what the problem is and why this PR fixes it.

@RikkiGibson
Copy link
Member Author

@dotnet/roslyn-compiler for review

1 similar comment
@RikkiGibson
Copy link
Member Author

@dotnet/roslyn-compiler for review


public class DemoCscBreaks
{
public List<string> WillBreak => MethodReturningLambda(() => field ?? new List<string>());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider verifying the final annotation for field.

}

[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/78932")]
public void Lambda_03()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add some more variations, such as setters, and instances where the lambda will incur nullable warnings.

Copy link
Member

@333fred 333fred left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done review pass (commit 4)

@RikkiGibson RikkiGibson requested review from a team and 333fred September 10, 2025 00:24
@RikkiGibson RikkiGibson requested a review from a team September 10, 2025 03:50
@RikkiGibson RikkiGibson merged commit 265c765 into dotnet:main Sep 11, 2025
24 checks passed
@RikkiGibson RikkiGibson deleted the field-lambda-crash branch September 11, 2025 18:55
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Sep 11, 2025
333fred added a commit that referenced this pull request Sep 12, 2025
* upstream/main: (201 commits)
  Handle extension blocks in CLS compliance checker (#80251)
  Fix
  Simplify
  Simplify
  Simplify
  Simplify
  Simplify
  Add doc for GetInferredNullableAnnotation (#80245)
  Rename files to match type within
  Implement ref local hoisting in runtime async. Closes #79763.
  Pull ref initialization hoisting into a reusable class for use in runtime async
  REvert
  Break cycle for lambda using field keyword with inferred return type (#79995)
  Simplify tests
  Simplify
  Fixup
  New extension
  Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API (#80170)
  In process
  Support 'add using' for modern extension methods
  ...
333fred added a commit to 333fred/roslyn that referenced this pull request Sep 12, 2025
* upstream/main: (1725 commits)
  Handle extension blocks in CLS compliance checker (dotnet#80251)
  Fix
  Simplify
  Simplify
  Simplify
  Simplify
  Simplify
  Add doc for GetInferredNullableAnnotation (dotnet#80245)
  Rename files to match type within
  Implement ref local hoisting in runtime async. Closes dotnet#79763.
  Pull ref initialization hoisting into a reusable class for use in runtime async
  REvert
  Break cycle for lambda using field keyword with inferred return type (dotnet#79995)
  Simplify tests
  Simplify
  Fixup
  New extension
  Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API (dotnet#80170)
  In process
  Support 'add using' for modern extension methods
  ...
333fred added a commit to 333fred/roslyn that referenced this pull request Sep 12, 2025
* upstream/main: (131 commits)
  Handle extension blocks in CLS compliance checker (dotnet#80251)
  Fix
  Simplify
  Simplify
  Simplify
  Simplify
  Simplify
  Add doc for GetInferredNullableAnnotation (dotnet#80245)
  Rename files to match type within
  Implement ref local hoisting in runtime async. Closes dotnet#79763.
  Pull ref initialization hoisting into a reusable class for use in runtime async
  REvert
  Break cycle for lambda using field keyword with inferred return type (dotnet#79995)
  Simplify tests
  Simplify
  Fixup
  New extension
  Extensions: add SyntaxGenerator support and AssociatedExtensionImplementation API (dotnet#80170)
  In process
  Support 'add using' for modern extension methods
  ...
@akhera99 akhera99 modified the milestones: Next, 18.0 P1, 18.0 P2 Sep 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CSharp compiler breaks in nullable project if auto-generated "field" is used in a generic lambda

5 participants