Skip to content

Add initial support for Union matching#81188

Merged
AlekseyTs merged 3 commits into
dotnet:features/Unionsfrom
AlekseyTs:Unions_01
Nov 18, 2025
Merged

Add initial support for Union matching#81188
AlekseyTs merged 3 commits into
dotnet:features/Unionsfrom
AlekseyTs:Unions_01

Conversation

@AlekseyTs

@AlekseyTs AlekseyTs commented Nov 12, 2025

Copy link
Copy Markdown
Contributor

Relates to test plan #81074

@AlekseyTs AlekseyTs requested a review from a team as a code owner November 12, 2025 23:08
@AlekseyTs AlekseyTs requested a review from a team as a code owner November 12, 2025 23:08
@dotnet-policy-service dotnet-policy-service Bot added the Needs API Review Needs to be reviewed by the API review council label Nov 12, 2025
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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.

@AlekseyTs

Copy link
Copy Markdown
Contributor Author

@RikkiGibson, @333fred, @dotnet/roslyn-compiler Please review

@RikkiGibson RikkiGibson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Taking a look now.

(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

@333fred 333fred Nov 13, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
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

@333fred 333fred Nov 13, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
The preseeding union matching operation in evaluation order, if any, corresponds
The preceding union matching operation in evaluation order, if any, corresponds
``` #Resolved

Comment thread src/Compilers/CSharp/Portable/Binder/Binder_Patterns.cs
Comment thread src/Compilers/CSharp/Portable/Binder/Binder_Patterns.cs
{
partial class Binder
{
private NamedTypeSymbol? PrepareForUnionMatching(SyntaxNode node, ref TypeSymbol inputType, BindingDiagnosticBag diagnostics)

@333fred 333fred Nov 14, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Comment thread src/Compilers/CSharp/Portable/Binder/Binder_Patterns.cs
Comment thread src/Compilers/CSharp/Portable/Binder/UnionMatchingRewriter.cs

@333fred 333fred left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 });

@333fred 333fred Nov 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Think inverting would make this clearer:

Suggested change
Debug.Assert(node is not BoundPattern { IsUnionMatching: true });
Debug.Assert(node is BoundPattern { IsUnionMatching: false });
``` #Resolved

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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));

@333fred 333fred Nov 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Alternatively, it can be rewritten to just be .SelectAsArray(RewritePatternWithUnionMatchingToPropertyPattern).

Suggested change
ImmutableArray<BoundPattern> subpatterns = this.VisitList(node.Subpatterns).SelectAsArray(p => RewritePatternWithUnionMatchingToPropertyPattern(p));
ImmutableArray<BoundPattern> subpatterns = this.VisitList(node.Subpatterns).SelectAsArray(static p => RewritePatternWithUnionMatchingToPropertyPattern(p));
``` #Resolved

Comment thread src/Compilers/CSharp/Portable/Binder/UnionMatchingRewriter.cs
{
// Update LeftOfPendingConjunction with the conjunction of left and LeftOfPendingConjunction

// The code below unwraps the following reqursive operation:

@333fred 333fred Nov 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// The code below unwraps the following reqursive operation:
// The code below unwraps the following recursive operation:
``` #Resolved

, narrowedTypeCandidates
#endif
);
} while (binaryPatternStack.TryPop(out binaryPattern));

@333fred 333fred Nov 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
} while (binaryPatternStack.TryPop(out binaryPattern));
}
while (binaryPatternStack.TryPop(out binaryPattern));
``` #Resolved

Comment on lines +425 to +426
inputType: leftOfPendingConjunction?.NarrowedType ?? inputType,
narrowedType: leftOfPendingConjunction?.NarrowedType ?? inputType).MakeCompilerGenerated();

@333fred 333fred Nov 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: extract a common variable? #Resolved

Comment thread src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs
{
Pattern: { } nestedPattern,
Member:
{ Type.SpecialType: SpecialType.System_Object, Symbol: var symbol } and

@333fred 333fred Nov 17, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: perhaps unionValueSymbol instead of just symbol? #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

perhaps unionValueSymbol instead of just symbol?

Well, we don't know that it is a union value symbol. We are yet to check that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Then possibleUnionValueSymbol? Just symbol is rather vague.

@RikkiGibson RikkiGibson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Done review pass

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)

@RikkiGibson RikkiGibson Nov 18, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@RikkiGibson RikkiGibson Nov 18, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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[].

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Of course. My mistake. Thanks.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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;

@RikkiGibson RikkiGibson Nov 18, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I didn't follow why this is being done, could you please explain? #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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);

@RikkiGibson RikkiGibson Nov 18, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 333fred left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Generally looking good to me. A few comments on tests, but nothing that blocks merging.

Comment on lines +272 to +275
// 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"]);

@333fred 333fred Nov 18, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread src/Compilers/CSharp/Test/Emit3/UnionsTests.cs

struct S2<T>
{
public T Value;

@333fred 333fred Nov 18, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: Consider naming this something else, so I don't initially read it as IUnion.Value. Prop or the like. #WontFix

Comment thread src/Compilers/CSharp/Test/Emit3/UnionsTests.cs
Comment thread src/Compilers/CSharp/Test/Emit3/UnionsTests.cs
}
";
var comp = CreateCompilation([src, IUnionSource], targetFramework: TargetFramework.NetCoreApp);
// PROTOTYPE: It looks like list pattern cannot work with union types.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@RikkiGibson RikkiGibson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM (iteration 3).

@AlekseyTs AlekseyTs enabled auto-merge (squash) November 18, 2025 22:48
@AlekseyTs AlekseyTs merged commit e154e00 into dotnet:features/Unions Nov 18, 2025
28 of 29 checks passed

internal static bool IsWellKnownTypeIUnion(this NamedTypeSymbol typeSymbol)
=> typeSymbol.DeclaredAccessibility == Accessibility.Public &&
typeSymbol.IsInterface &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we check arity too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Compilers Feature - Unions Needs API Review Needs to be reviewed by the API review council

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants