Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for generating fields/properties for property patterns. #10873

Conversation

CyrusNajmabadi
Copy link
Member

Fixes #9090

@CyrusNajmabadi
Copy link
Member Author

Tagging @dotnet/roslyn-ide

@gafter
Copy link
Member

gafter commented Apr 26, 2016

@CyrusNajmabadi This will have to wait until after #10866; I'm removing property patterns from the code base before insertion into future, and any changes to the pattern-matching code will cause merge heck.
#Closed

@DustinCampbell
Copy link
Member

DustinCampbell commented Dec 1, 2016

@CyrusNajmabadi, @gafter: Should we close this ancient PR until we're ready for property patterns? #Closed

@sharwell sharwell added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Sep 8, 2017
@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Sep 22, 2017

@CyrusNajmabadi, @gafter: Should we close this ancient PR until we're ready for property patterns?

Works for me. If we add property patterns back in someone can resurrect this :) #Closed

@CyrusNajmabadi CyrusNajmabadi changed the base branch from features/patterns to features/recursive-patterns March 28, 2018 00:19
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner March 28, 2018 00:20
@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Mar 28, 2018

@gafter Can you give me a set of interesting examples that demonstrate how people can use recursive patterns? I want to make sure this features handles all the ones that make sense. Thanks! #Closed

@gafter
Copy link
Member

gafter commented Mar 28, 2018

Yes, I will prepare a set of examples. #Closed

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Mar 28, 2018

Awesome, thanks. It doesn't have to be huge. Just like 1 (or 2) tops, for each interesting new construct. And maybe some special 'oddities' (if they exist) to be aware of. #Closed

@gafter
Copy link
Member

gafter commented Mar 29, 2018

I will include some complete examples here in a moment, but just to point out some oddities first...

A property pattern checks that the input value is non-null, and that each named property matches the given pattern. But you can use no patterns if you just want a non-null check:

if (o is {})

The switch expression (parser and code gen) has some fixes pending that are not integrated yet, so it is broken in features/recursive-patterns at this moment, so better to focus on the switch statement and is-pattern expression. #Resolved

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Mar 29, 2018

if (o is {})

Good case. Just to verify, you should be able to write the following right:

if (o is { [|X|] is int i })

And this just means: "'o' is non-null and has a field/property 'X' that matches the pattern 'int i'"?

#Resolved

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Mar 29, 2018

Also, do we have a list of all patterns written down somewhere, i'd like to test out a variety of combinations. What i know of off the top of my head:

  1. Type pattern: int i
  2. Constant pattern: 0
  3. Allow-null-of-any-type-pattern: var x
  4. non-null property pattern: { ... }
  5. non-null + type + property pattern: X { ... }
  6. Discard pattern: _

Did we do deconstruction patterns? i.e. something like SomeType(...)? If so, what syntax did we settle on for that?

#Resolved

@gafter
Copy link
Member

gafter commented Mar 29, 2018

Here is a little sample demonstrating the interaction of tuples and patterns. Possibly not relevant to this PR

using System;

public class Door
{
    public DoorState State;

    public enum DoorState { Opened, Closed, Locked }

    public enum Action { Open, Close, Lock, Unlock }

    public void Act(Action action, bool haveKey = false)
    {
        Console.Write($"{State} {action}{(haveKey ? " withKey" : null)}");
        State = ChangeState0(State, action, haveKey);
        Console.WriteLine($" -> {State}");
    }

    public static DoorState ChangeState0(DoorState state, Action action, bool haveKey = false)
    {
        switch ((state, action))
        {
            case (DoorState.Opened, Action.Close):
                return DoorState.Closed;
            case (DoorState.Closed, Action.Open):
                return DoorState.Opened;
            case (DoorState.Closed, Action.Lock) when haveKey:
                return DoorState.Locked;
            case (DoorState.Locked, Action.Unlock) when haveKey:
                return DoorState.Closed;
            case var (oldState, _):
                return oldState;
        }
    }

    public static DoorState ChangeState1(DoorState state, Action action, bool haveKey = false) =>
        (state, action) switch {
            (DoorState.Opened, Action.Close) => DoorState.Closed,
            (DoorState.Closed, Action.Open) => DoorState.Opened,
            (DoorState.Closed, Action.Lock) when haveKey => DoorState.Locked,
            (DoorState.Locked, Action.Unlock) when haveKey => DoorState.Closed,
            _ => state };
}

class Program
{
    static void Main(string[] args)
    {
        var door = new Door();
        door.Act(Door.Action.Close);
        door.Act(Door.Action.Lock);
        door.Act(Door.Action.Lock, true);
        door.Act(Door.Action.Open);
        door.Act(Door.Action.Unlock);
        door.Act(Door.Action.Unlock, true);
        door.Act(Door.Action.Open);
    }
}

#Resolved

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Mar 29, 2018

Also, inside a property-pattern, what syntax are the field/property pattern matchers? At one point it was { X is int i }, is that still the case? or did we go with another syntax that isn't just a list of "IsPatternExpression"s? #Resolved

@gafter
Copy link
Member

gafter commented Mar 29, 2018

Here is a possibly more relevant sample, though there are only a couple of examples of property patterns in it.

using System;

class Program2
{
    static StatementSyntax s =
        new ExpressionStatementSyntax(
            new AssignmentExpressionSyntax(
                new SimpleNameSyntax(new SyntaxToken("x")),
                new SimpleNameSyntax(new SyntaxToken("x"))));

    public static void Test()
    {
        if (s is
            ExpressionStatementSyntax(
                AssignmentExpressionSyntax(
                    SimpleNameSyntax(SyntaxToken(var leftName)),
                    SimpleNameSyntax(SyntaxToken(var rightName))
                    ) a) &&
            leftName == rightName)
        {
            Console.WriteLine("self-assignment!");
        }
    }

    public static void Test2()
    {
        if (s is
            ExpressionStatementSyntax(
                AssignmentExpressionSyntax(
                    SimpleNameSyntax { Identifier: SyntaxToken(var leftName) },
                    SimpleNameSyntax { Identifier: SyntaxToken(var rightName) }
                    ) a) &&
            leftName == rightName)
        {
            Console.WriteLine("self-assignment!");
        }
    }

    public static void Test3()
    {
        if (s is
            ExpressionStatementSyntax(
                AssignmentExpressionSyntax(
                    SimpleNameSyntax(var leftToken),
                    SimpleNameSyntax(var rightToken)
                    ) a) &&
            leftToken.ValueText == rightToken.ValueText)
        {
            Console.WriteLine("self-assignment!");
        }
    }
}











abstract class SyntaxNode
{
    public int Location => 0;
}
class SyntaxToken : SyntaxNode
{
    public string ValueText;
    public SyntaxToken(string ValueText) => this.ValueText = ValueText;
    public void Deconstruct(out string ValueText) => ValueText = this.ValueText;
}
class StatementSyntax : SyntaxNode
{
}
class ExpressionSyntax : SyntaxNode
{
}
class ExpressionStatementSyntax : StatementSyntax
{
    public ExpressionSyntax Expression;
    public ExpressionStatementSyntax(ExpressionSyntax Expression) => this.Expression = Expression;
    public void Deconstruct(out ExpressionSyntax Expression) => Expression = this.Expression;
}
class AssignmentExpressionSyntax : ExpressionSyntax
{
    public ExpressionSyntax Left;
    public ExpressionSyntax Right;
    public AssignmentExpressionSyntax(ExpressionSyntax Left, ExpressionSyntax Right) => (this.Left, this.Right) = (Left, Right);
    public void Deconstruct(out ExpressionSyntax Left, out ExpressionSyntax Right) => (Left, Right) = (this.Left, this.Right);
}
class SimpleNameSyntax : ExpressionSyntax
{
    public SyntaxToken Identifier;
    public SimpleNameSyntax() { }
    public SimpleNameSyntax(SyntaxToken Identifier) => this.Identifier = Identifier;
    public void Deconstruct(out SyntaxToken Identifier) => Identifier = this.Identifier;
}

#Resolved

@gafter gafter self-assigned this Mar 29, 2018
@gafter gafter added this to the 16.0 milestone Mar 29, 2018
@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Mar 29, 2018

Ah interesting:

is ExpressionStatementSyntax(
                AssignmentExpressionSyntax(
                    SimpleNameSyntax { Identifier: SyntaxToken(var leftName) },
                    SimpleNameSyntax { Identifier: SyntaxToken(var rightName) }
                    ) a)

So the "ExpressionStatementSyntax ( .. )``` is a type+deconstruction pattern? Ensure that the value is of htat type, and if so, deconstruct it and check if the deconstructed parts match teh subpatterns?

Then Identifier: SyntaxToken(var leftName) is a field/property checking pattern in a property-pattern? So the syntax is "Name pattern"? #Resolved

@gafter
Copy link
Member

gafter commented Mar 30, 2018

Yes, you've got that. In its most general form, a recursive pattern can be

Type ( SubpatternList ) { PropertySubpatternList } Identifier

You can omit any of the parts, except that you can't have only a type, only an identifier, or nothing.
#Resolved

@gafter
Copy link
Member

gafter commented Mar 30, 2018

See also dotnet/csharplang#1054 and note that we added a shorthand discard pattern _. #Resolved

@CyrusNajmabadi
Copy link
Member Author

Tagging @dotnet/roslyn-ide Please review this update of "generate variable" to understand and properly generate properties when used in property-patterns.

@CyrusNajmabadi
Copy link
Member Author

@dotnet/roslyn-ide @jmarolf @jcouv can someone take a look at this PR? Thanks!


class Blah
{
public int X { get; internal set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

internal is redundant here

public int X { get; internal set; }
}
}", parseOptions: WithPatternsEnabled);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

consider adding tests with some error cases, eg. Blah { X: { [|Y|]: int i } } where X doesn't exist, and consider adding an example with more than 1 property

Copy link
Member Author

Choose a reason for hiding this comment

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

consider adding tests with some error cases, eg. Blah { X: { [|Y|]: int i } } where X doesn't exist,
sure

and consider adding an example with more than 1 property

It's unclear what that would be testing. We don't generate multiple props at a time for example.

Copy link
Contributor

Choose a reason for hiding this comment

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

it would be testing that this doesn't crash

{
return IsObjectInitializerNamedAssignmentIdentifier(node, out var unused);
}
=> IsObjectInitializerNamedAssignmentIdentifier(node, out var unused);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: discard

Copy link
Member Author

Choose a reason for hiding this comment

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

sure.

node.Parent.IsParentKind(SyntaxKind.SubpatternElement);

public bool IsPropertySubpattern(SyntaxNode node)
=> node.Kind() == SyntaxKind.PropertySubpattern;
Copy link
Contributor

Choose a reason for hiding this comment

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

does this belong here even though it's specific to C#?

Copy link
Member Author

Choose a reason for hiding this comment

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

SyntaxFacts is a superset of both languages. Either language can simply return 'false' for anything they dont' support.

@@ -154,6 +154,17 @@ protected AbstractGenerateMemberService()
isStatic = false;
return;
}
else if (syntaxFacts.IsNameOfSubpatternElement(expression) &&
syntaxFacts.IsPropertySubpattern(expression.Parent.Parent.Parent))
Copy link
Contributor

@Neme12 Neme12 Apr 12, 2018

Choose a reason for hiding this comment

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

expression.Parent.Parent.Parent is really not clear here... if you want to think of syntaxFacts as language agnostic, this kind of check doesn't make sense in this place. Consider adding an out argument for subpatternElement to IsNameOfSubpatternElement so you could only do one Parent check.

Copy link
Member Author

Choose a reason for hiding this comment

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

sure.

}

private static bool DecomposeBinaryOrAssignmentExpression(ExpressionSyntax expression, out SyntaxToken operatorToken, out ExpressionSyntax left, out ExpressionSyntax right)
private static bool DecomposeBinaryOrAssignmentExpression(SyntaxNode node, out SyntaxToken operatorToken, out ExpressionSyntax left, out ExpressionSyntax right)
Copy link
Contributor

Choose a reason for hiding this comment

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

why was this changed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because the type inferred now works across expressions and patterns uniformly.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see how that's relevant here. This method is called DecomposeBinaryOrAssignmentExpression and still only handles expressions.

Copy link
Member Author

Choose a reason for hiding this comment

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

sorry, i thought you were asking a general question about the use of Expression vs SyntaxNode. I can make this ExpressionSyntax, though then i still have to add casts. That's not a big deal, just something that wasn't necessary if this was typed as SyntaxNode.

if (expression != null)
{
expression = expression.WalkUpParentheses();
node = expression;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: consider pattern matching: if (node is ExpressionSyntax expression) node = expression.WalkUpParentheses()

@@ -899,7 +909,7 @@ private int GetArgumentListIndex(AttributeArgumentListSyntax attributeArgumentLi
return (tokenIndex + 1) / 2;
}

private IEnumerable<TypeInferenceInfo> InferTypeInBinaryOrAssignmentExpression(ExpressionSyntax binop, SyntaxToken operatorToken, ExpressionSyntax left, ExpressionSyntax right, ExpressionSyntax expressionOpt = null, SyntaxToken? previousToken = null)
private IEnumerable<TypeInferenceInfo> InferTypeInBinaryOrAssignmentExpression(SyntaxNode binop, SyntaxToken operatorToken, ExpressionSyntax left, ExpressionSyntax right, ExpressionSyntax expressionOpt = null, SyntaxToken? previousToken = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like it should still be ExpressionSyntax

Copy link
Member Author

Choose a reason for hiding this comment

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

true. fixed.

var memberTypes =
parentTypes.SelectMany(ti => GetMemberReturnTypes(ti.InferredType, identifier.Identifier.ValueText))
.SelectAsArray(t => new TypeInferenceInfo(t));

Copy link
Contributor

Choose a reason for hiding this comment

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

why is all this logic necessary? doesn't the compiler return a symbol for X?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. Let me try that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hrm.. Doesn't work. Not sure if that's intentional or not. @gafter can you shed some light here? The scenario is the following code:

    void M2()
    {
        object o = null;
        if (o is Blah { X: { Y: int i } })  //<-- call semanticModel.GetSymbolInfo on 'X' here.
        {
        }
    }

    class Frob { }

    class Blah
    {
        public Frob X;
    }

If I call semanticModel.GetSymbolInfo(id) on the identifier node for 'X', i don't get anything back. Is that expected? Or should i get back the the field symbol declared inside Blah? My intuition is 'yes', but i know the compiler has some very particular views about types/symbols and whatnot in patterns.

Can you clarify the expected behavior here of hte semantic model? Thanks!

Copy link
Member

Choose a reason for hiding this comment

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

The semantic model is not implemented for this syntactic location yet.

Copy link
Member Author

Choose a reason for hiding this comment

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

@gafter Is the expectation that it will be at some later point? I just want to know what i should expect to work in the future so i can write the feature code in the appropriate manner :)

For example, you explained in the past how patterns don't have types, so i know to not even expect to be able to use .GetTypeInfo on a pattern. But the rules about things like getting symbol info on these parts of a pattern are a bit less clear.

Thanks!

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I intend that identifiers referenced within a pattern will have SymbolInfo working. I will work on that next (though I am out for about a week, so it will be more than a week before I have it working).

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds good. I'll wait for that to be done.

}

private ImmutableArray<ITypeSymbol> GetMemberReturnTypes(ITypeSymbol type, string memberName)
{
Copy link
Contributor

Choose a reason for hiding this comment

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

static

Copy link
Member Author

Choose a reason for hiding this comment

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

done.


class Blah
{
public Frob X;
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a similar test wehre X is a property rather than field?

Copy link
Contributor

@Neme12 Neme12 Apr 12, 2018

Choose a reason for hiding this comment

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

it would be good to have one to exercise both cases in GetMemberReturnTypes

@jaredpar jaredpar added the Resolution-Expired The request is closed due to inactivity under our contribution review policy. label Apr 19, 2018
@jaredpar
Copy link
Member

We apologize, but we are closing this PR due to code drift. We regret letting this PR go so long without attention, but at this point the code base has changed too much for us to revisit older PRs. Going forward, we will manage PRs better under an SLA currently under draft in issue #26266 – we encourage you to add comments to that draft as you see fit. Although that SLA is still in draft form, we nevertheless want to move forward with the identification of older PRs to give contributors as much early notice as possible to review and update them.

If you are interested in pursuing this PR, please reset it against the head of master and we will reopen it. Thank you!

@jaredpar jaredpar closed this Apr 19, 2018
@CyrusNajmabadi
Copy link
Member Author

@jaredpar please reopen this PR. This is work that @gafter has asked me to do as part of the pattern-matching feature work.

If you are interested in pursuing this PR, please reset it against the head of master and we will reopen it. Thank you!

This PR is intentionally against @gafter's feature branch as that's where all the pattern work is happening.

Note that work has been made in this PR as of a week ago, please don't close the PRs that are still actively being worked on, thanks :D

@jaredpar
Copy link
Member

@CyrusNajmabadi it's apparently not just you. I can't re-open this PR either. I'm not sure what is going on here.

@sharwell
Copy link
Member

sharwell commented Apr 19, 2018

@CyrusNajmabadi You need to create a branch generateVarPatterns at commit d9a8bf3 in your fork in order for this pull request to be reopened.

@CyrusNajmabadi
Copy link
Member Author

@sharwell can you clarify that? I already have a branch called generateVarExpressions . So i can't create one. Should i create another branch with a different name? Should i delete the existing branch? A set of steps would be appreciated here.

@CyrusNajmabadi
Copy link
Member Author

@jaredpar Can we put a hold on closing out old PRs until a set of reasonable steps can be provided to contributers on how to actually reopen things? Thanks!

@jaredpar
Copy link
Member

@CyrusNajmabadi this seems to be the only PR that was affected. I've re-opened several others with no ill effects.

@CyrusNajmabadi
Copy link
Member Author

CyrusNajmabadi commented Apr 19, 2018

Perhaps this is because it's a feature branch? I also don't seem to be able to reopen: #10420

@CyrusNajmabadi
Copy link
Member Author

I could not figure out how to reopen this. So i've opened #26355 instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-IDE cla-already-signed Community The pull request was submitted by a contributor who is not a Microsoft employee. New Language Feature - Pattern Matching Pattern Matching Resolution-Expired The request is closed due to inactivity under our contribution review policy.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants