Add initial support for Union matching#81188
Conversation
|
This PR modifies public API files. Please follow the instructions at https://github.com/dotnet/roslyn/blob/main/docs/contributing/API%20Review%20Process.md for ensuring all public APIs are reviewed before merging. |
|
@RikkiGibson, @333fred, @dotnet/roslyn-compiler Please review |
| (LeftOfPendingConjunction is null), or a conjunction with a Union matching operation | ||
| on the right hand side. The left hand side of the conjunction is then | ||
| represented by the LeftOfPendingConjunction child, and it is not applied | ||
| to the ValueProperty. It represents a pattern that is matched emmidiately |
There was a problem hiding this comment.
| to the ValueProperty. It represents a pattern that is matched emmidiately | |
| to the ValueProperty. It represents a pattern that is matched immediately | |
| ``` #Resolved |
|
|
||
| A Union matching operation corresponding to the top most BoundPatternWithUnionMatching node | ||
| represents the last Union matching operation in evaluation order in the entire hierarchy. | ||
| The preseeding union matching operation in evaluation order, if any, corresponds |
There was a problem hiding this comment.
| The preseeding union matching operation in evaluation order, if any, corresponds | |
| The preceding union matching operation in evaluation order, if any, corresponds | |
| ``` #Resolved |
| { | ||
| partial class Binder | ||
| { | ||
| private NamedTypeSymbol? PrepareForUnionMatching(SyntaxNode node, ref TypeSymbol inputType, BindingDiagnosticBag diagnostics) |
There was a problem hiding this comment.
Not sure how I feel about the naming of this method. I think TryGetUnionTypeForMatching or similar would be a better name, I'd expect a method with the name Prepare to actually return nothing. #Resolved
333fred
left a comment
There was a problem hiding this comment.
Done implementation review, only some minor comments. Will start reviewing tests tomorrow.
|
|
||
| public override BoundNode? Visit(BoundNode? node) | ||
| { | ||
| Debug.Assert(node is not BoundPattern { IsUnionMatching: true }); |
There was a problem hiding this comment.
Think inverting would make this clearer:
| Debug.Assert(node is not BoundPattern { IsUnionMatching: true }); | |
| Debug.Assert(node is BoundPattern { IsUnionMatching: false }); | |
| ``` #Resolved |
There was a problem hiding this comment.
Think inverting would make this clearer
I do not mean to assert that node is a pattern, which the suggestion would do. I mean to assert that it is not a pattern utilizing union matching. It is quite possible that this visitor can handle only patterns today, but that is orthogonal and I intentionally do not try to include this into the assert.
There was a problem hiding this comment.
Well, the very next assertion is that the visitor only handles very specific pattern nodes, so I think it would be fine to strengthen this assertion and make it more readable at the same time.
There was a problem hiding this comment.
I do not intend to mix things and find current assert simple and clear enough.
| public override BoundNode? VisitListPattern(BoundListPattern node) | ||
| { | ||
| Symbol? variable = node.Variable; | ||
| ImmutableArray<BoundPattern> subpatterns = this.VisitList(node.Subpatterns).SelectAsArray(p => RewritePatternWithUnionMatchingToPropertyPattern(p)); |
There was a problem hiding this comment.
Alternatively, it can be rewritten to just be .SelectAsArray(RewritePatternWithUnionMatchingToPropertyPattern).
| ImmutableArray<BoundPattern> subpatterns = this.VisitList(node.Subpatterns).SelectAsArray(p => RewritePatternWithUnionMatchingToPropertyPattern(p)); | |
| ImmutableArray<BoundPattern> subpatterns = this.VisitList(node.Subpatterns).SelectAsArray(static p => RewritePatternWithUnionMatchingToPropertyPattern(p)); | |
| ``` #Resolved |
| { | ||
| // Update LeftOfPendingConjunction with the conjunction of left and LeftOfPendingConjunction | ||
|
|
||
| // The code below unwraps the following reqursive operation: |
There was a problem hiding this comment.
| // The code below unwraps the following reqursive operation: | |
| // The code below unwraps the following recursive operation: | |
| ``` #Resolved |
| , narrowedTypeCandidates | ||
| #endif | ||
| ); | ||
| } while (binaryPatternStack.TryPop(out binaryPattern)); |
There was a problem hiding this comment.
| } while (binaryPatternStack.TryPop(out binaryPattern)); | |
| } | |
| while (binaryPatternStack.TryPop(out binaryPattern)); | |
| ``` #Resolved |
| inputType: leftOfPendingConjunction?.NarrowedType ?? inputType, | ||
| narrowedType: leftOfPendingConjunction?.NarrowedType ?? inputType).MakeCompilerGenerated(); |
There was a problem hiding this comment.
Nit: extract a common variable? #Resolved
| { | ||
| Pattern: { } nestedPattern, | ||
| Member: | ||
| { Type.SpecialType: SpecialType.System_Object, Symbol: var symbol } and |
There was a problem hiding this comment.
Nit: perhaps unionValueSymbol instead of just symbol? #Closed
There was a problem hiding this comment.
perhaps unionValueSymbol instead of just symbol?
Well, we don't know that it is a union value symbol. We are yet to check that.
There was a problem hiding this comment.
Then possibleUnionValueSymbol? Just symbol is rather vague.
| Diagnostic(ErrorCode.ERR_UseDefViolation, "y").WithArguments("y").WithLocation(29, 16), | ||
| // (34,22): error CS8121: An expression of type 'S1?' cannot be handled by a pattern of type 'int'. | ||
| // if (u is not int z) | ||
| Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1?", "int").WithLocation(34, 22) |
There was a problem hiding this comment.
I am curious what a pattern like u is not (S1 and int), or u is not ({ } and int) would do, if a "step by step" type narrowing would work, essentially. #Resolved
There was a problem hiding this comment.
Adding scenarios to UnionMatching_14_Negated
| } | ||
| "; | ||
| var comp = CreateCompilation([src, IUnionSource], targetFramework: TargetFramework.NetCoreApp); | ||
| // PROTOTYPE: It looks like list pattern cannot work with union types. |
There was a problem hiding this comment.
I found it surprising that even prefixing with a type, e.g. u is int[] [10], didn't make things work. It seems inconsistent with recursive patterns. See related scenario in .NET Lab #Resolved
There was a problem hiding this comment.
I found it surprising that even prefixing with a type,
I might be missing something, but I think a list pattern doesn't include a type. Therefore, obj is string[] [] is a type check against an array of string[].
There was a problem hiding this comment.
Of course. My mistake. Thanks.
There was a problem hiding this comment.
Right. It would have to be a conjunction, is int[] and [10]. We debating making list patterns part of the definition of a recursive pattern, but this syntax was a major "well how do we make that work" issue.
|
|
||
| if (unionType is { TypeKind: not TypeKind.Struct }) | ||
| { | ||
| underIsPattern = false; |
There was a problem hiding this comment.
I didn't follow why this is being done, could you please explain? #Closed
There was a problem hiding this comment.
I didn't follow why this is being done, could you please explain?
When we are not underIsPattern, we don't permitDesignations. See an assignment and a comment below. Only BindIsPatternExpression passes true for underIsPattern and only BindUnaryPattern propagates it.
Since when we are doing Union matching, we are effectively in a sub-pattern, we are not directly under "is pattern", but we can still make things work for struct types, because they do not have an implicit null check for the Union instance itself. The effect of this logic can be observed in unit-tests UnionMatching_15_Negated and UnionMatching_16_Negated, for example.
| Debug.Assert(!inputType.IsNullableType()); | ||
| Debug.Assert(!negated); | ||
|
|
||
| negated ^= nestedPattern.IsNegated(out var negatedNestedPattern); |
There was a problem hiding this comment.
It looks like 'IsNegated' no longer digs through all the negations when union matching is involved, do the other existing call sites of IsNegated need to be updated to account for this? #Resolved
There was a problem hiding this comment.
It looks like 'IsNegated' no longer digs through all the negations when union matching is involved, do the other existing call sites of IsNegated need to be updated to account for this?
I do not think any other places need an adjustment. This place needed an adjustment due to the fact that DefiniteAssignmentPass.VisitPattern never considers pattern locals under NegatedPattern as definitely assigned. In this particular situation they should be considered definitely assigned. Therefore, we perform a semantically equivalent rewrite by pulling negation out of the sub-pattern. When applied to a struct type a pattern {Value: not (...) } is equivalent to a pattern not {Value: (...) }
333fred
left a comment
There was a problem hiding this comment.
Generally looking good to me. A few comments on tests, but nothing that blocks merging.
| // PROTOTYPE: This looks strange. C5 simply inherits from C1 and because of that it is treated as a union type. | ||
| // It doesn't change what IUnion.Value returns, but its constructors are treated as though they | ||
| // define possible types returned by IUnion.Value. | ||
| VerifyCaseTypes(comp, "C5", ["System.String"]); |
There was a problem hiding this comment.
Definitely agreed that this should be discussed, but fwiw, my initial reaction is that redefinition of Value shouldn't drive whether constructors are considered to "add" to the list of valid cases. It feels a tad opaque and "spooky action at a distance" to me. #Resolved
There was a problem hiding this comment.
I don't want to go into a discussion in context of this PR, but nothing is redefined here. We simply inherit from a Union class, that is all that is needed to mess things up.
|
|
||
| struct S2<T> | ||
| { | ||
| public T Value; |
There was a problem hiding this comment.
Nit: Consider naming this something else, so I don't initially read it as IUnion.Value. Prop or the like. #WontFix
| } | ||
| "; | ||
| var comp = CreateCompilation([src, IUnionSource], targetFramework: TargetFramework.NetCoreApp); | ||
| // PROTOTYPE: It looks like list pattern cannot work with union types. |
There was a problem hiding this comment.
Right. It would have to be a conjunction, is int[] and [10]. We debating making list patterns part of the definition of a recursive pattern, but this syntax was a major "well how do we make that work" issue.
|
|
||
| internal static bool IsWellKnownTypeIUnion(this NamedTypeSymbol typeSymbol) | ||
| => typeSymbol.DeclaredAccessibility == Accessibility.Public && | ||
| typeSymbol.IsInterface && |
There was a problem hiding this comment.
Should we check arity too?
Thanks. I think we should. This function is not fully tested. I'll add a PROTOTYPE comment for that. It looks like many similar functions in this file are missing the Arity check. I'll open an issue for that.
Relates to test plan #81074