Skip to content

Don't read the same property twice in pattern matching #35669

Merged
gafter merged 6 commits intodotnet:masterfrom
YairHalberstadt:pattern-matching-reduce-number-of-reads
Jun 3, 2019
Merged

Don't read the same property twice in pattern matching #35669
gafter merged 6 commits intodotnet:masterfrom
YairHalberstadt:pattern-matching-reduce-number-of-reads

Conversation

@YairHalberstadt
Copy link
Copy Markdown
Contributor

when accessed through the same underlying object, but references of different types.

Fix to #34933

Namely:

using System;

public class Person {
    public string Name { get; set; }
}

public class Student : Person { }


public class C {
    public void M(Person p) {
        switch (p) {
            case { Name: "Bill" }:
                Console.WriteLine("Hey Bill!");
                break;
            case Student { Name: var name }:
                Console.WriteLine($"Hello student {name}!");
                break;
            case { Name: var name }:
                Console.WriteLine($"Hello non-student {name}!");
                break;
        }
    }
}

will now only evaluate Person.Name once.

There are a number of limitations to this - the tests describe the details in comments.

…through the same underlying object, but references of different types.
@YairHalberstadt YairHalberstadt requested a review from a team as a code owner May 12, 2019 12:26
@@ -9,7 +9,7 @@ partial class BoundDagEvaluation
public override bool Equals(object obj) => obj is BoundDagEvaluation other && this.Equals(other);
public virtual bool Equals(BoundDagEvaluation other)
{
Copy link
Copy Markdown
Contributor Author

@YairHalberstadt YairHalberstadt May 12, 2019

Choose a reason for hiding this comment

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

Because we're overriding object.Equals, it's impossible to know statically everywhere this is called.
Instead I've set a breakpoint here, and on GetHashCode, and I've seen where it's been called from in the tests I added.

Namely:

DecisionDagBuilder.SimplifyTestsAndBindings(ArrayBuilder<BoundDagTest> testsBuilder, ArrayBuilder<BoundPatternBinding> bindingsBuilder)
     usedValues

DecisionDagBuilder.RemoveEvaluation(RemainingTestsForCase c, BoundDagEvaluation e)
   RemainingTests: c.RemainingTests.WhereAsArray(d => !(d is BoundDagEvaluation e2) || e2 != e),

NullableWalker.LearnFromDecisionDag()
    tempMap

LocalRewriter.PatternLocalRewriter
    _map

I was not able to understand what was happening in NullableWalker.LearnFromDecisionDag, so I've simply added tests to verify that my changes do not affect nullability analysis. I imagine @gafter would be able to help here.

In all other cases my changes to Equals seemed correct.
#Resolved

Copy link
Copy Markdown
Contributor Author

@YairHalberstadt YairHalberstadt May 13, 2019

Choose a reason for hiding this comment

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

Thanks to @jcouv for giving me an overview of how the nullable walker works. It seems to me that this behaviour is correct - since we assume property evaluations are non-mutating, the nullability state of any evaluations of the same property through different types should be the same, so it's ok to use the same slot for them. #Resolved

@gafter gafter self-assigned this May 13, 2019
@gafter gafter added this to the 16.2 milestone May 13, 2019
/// So two evaluations act on the same input so long as they have the same original input.
/// </summary>
/// <returns></returns>
private BoundDagTemp GetOriginalInput()
Copy link
Copy Markdown
Member

@gafter gafter May 13, 2019

Choose a reason for hiding this comment

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

Please remove the empty <returns> section. #Resolved


public class Student : Person {
public override string Name { get => Name; }
}
Copy link
Copy Markdown
Member

@gafter gafter May 13, 2019

Choose a reason for hiding this comment

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

This looks like infinite recursion. Did you mean base.Name? #Resolved

Copy link
Copy Markdown
Member

@gafter gafter left a comment

Choose a reason for hiding this comment

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

:shipit:

@gafter
Copy link
Copy Markdown
Member

gafter commented May 13, 2019

@agocke Could you look at this please?

@YairHalberstadt You may have to pull the latest from the master branch to fix the build error in the CI tests. #Resolved

@YairHalberstadt
Copy link
Copy Markdown
Contributor Author

YairHalberstadt commented May 14, 2019

This might cause another issue in NullableWalker.LearnFromDecisionDag().
There we call NullableWalker.AsMemberOfType() on the property of a BoundDagPropertyEvaluation. We then pass that property into GetOrCreateSlot(). It is thus theoretically possible that if two BoundDagPropertyEvaluation have different input types, that they will be given different slots. However tempmap will consider them equal now.

It doesn't look like GetOrCreateSlot() depends on the changes NullableWalker.AsMemberOfType() makes to the property, however I do not know the purpose of NullableWalker.AsMemberOfType() (i.e. why it's necessary), so I couldn't say for sure. I think @cston worked on it. Would you be able to explain it to me? Thanks.

As an aside, I think that there is limited documentation of the Nullable Walker. @jcouv gave me a high level overview a couple of days ago. I would be happy to add that information to the Nullable Walker, and will also try and add any information @cston gives.

@jcouv's overview was:

NullableWalker visits bound nodes from initial binding (ie. with actual nullabilities only for explicit ones in source such as string? local = "";, but all inferred nullabilities left as oblivious such as var local = "";) to:

compute the nullability of expressions (and save those for semantic model),
keep track of knowledge for different variables (more on that below), and
produce warnings.
Slots and state are how it keeps track of knowledge for variables, or more generally storage locations. A slot is just a position/index into the state array.
For instance, parameters and locals in a method get slot, which holds either non-null or maybe-null state. Consider a parameter string? p1, we give it a slot/index and we'll initialize its state to maybe-null (ie. State[slot] = MaybeNull, because its declared type or TypeWithAnnotations is NotAnnotated), then when we visit p1 = ""; we can just override that state, and when we visit p1.ToString() we consult that state to decide whether to warn.

We also track fields within structs, so we also make slots for those. That way we can warn on localStruct.field1.ToString(), but not localStruct.field2.ToString() independently.
I don't remember how that's represented, but slots are related to each other, so that when assigning local2 = local1; we can not only copy the slot for local1to set the state of local2, but we can copy the nested slots.

It's worth noting that the state is generally just a simple array, but it can also be two arrays in some cases. That's called "conditional state". It's is used for analyzing expressions like x == null. We keep track of the state "if the expression were true" and "if the expression were false" separately. Slots are still used to index into those arrays as normal, but if I recall correctly it is good to ensure that slots get assigned before entering such a state.
Another operation that is common is that of cloning states. If you analyze if (b) ... else ..., then we clone the state so that we can analyze each branch separately. Then there are rules for merging states when the branches rejoin (Meet takes the worst case values).

There are rules for branches that are not reachable if (false) { ... unreachable ...}. In such unreachable code, every value you read is NotNull regardless of current state. #Resolved

@cston
Copy link
Copy Markdown
Contributor

cston commented May 14, 2019

@YairHalberstadt, AsMemberOfType() is used to get the corresponding member for a symbol from initial binding to match an updated receiver type in NullableWalker.

For instance, AsMemberOfType() maps from List<string~>.Add(string~) to List<string?>.Add(string?):

string s = null;
var list = new[] { s }.ToList();
list.Add(null);

GetOrCreateSlot(Symbol symbol, int containingSlot) ignores the type arguments for constructed members though. It only depends on symbol.OriginalDefinition (and containingSlot). #Resolved

@RikkiGibson RikkiGibson requested a review from agocke May 17, 2019 21:29
@gafter gafter requested a review from cston May 21, 2019 21:31
@gafter
Copy link
Copy Markdown
Member

gafter commented May 21, 2019

@cston Could you please review this? I'd be happy to walk through it with you if it helps. #Resolved

@@ -9,7 +9,7 @@ partial class BoundDagEvaluation
public override bool Equals(object obj) => obj is BoundDagEvaluation other && this.Equals(other);
public virtual bool Equals(BoundDagEvaluation other)
Copy link
Copy Markdown
Contributor

@cston cston May 31, 2019

Choose a reason for hiding this comment

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

Equals [](start = 28, length = 6)

Logged #36102 to consider renaming the Equals method to something more specific. (That shouldn't block this PR.) #Resolved

[Fact, WorkItem(34933, "https://github.com/dotnet/roslyn/issues/34933")]
public void NoRedundantPropertyRead04()
{
// Cannot combine the evalations of name here, since we first check if p is Teacher,
Copy link
Copy Markdown
Contributor

@cston cston May 31, 2019

Choose a reason for hiding this comment

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

evalations [](start = 34, length = 10)

Typo, here and other tests. #Resolved

var source = @"using System;

public class Person {
public string Name { get; set; }
Copy link
Copy Markdown
Contributor

@cston cston May 31, 2019

Choose a reason for hiding this comment

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

public string Name { get; set; } [](start = 4, length = 32)

Consider testing with abstract or virtual property with no override in derived type. #Resolved

@YairHalberstadt
Copy link
Copy Markdown
Contributor Author

YairHalberstadt commented Jun 3, 2019

@cston
I've made your requested changes. #Resolved

@gafter gafter merged commit 59b717d into dotnet:master Jun 3, 2019
@gafter
Copy link
Copy Markdown
Member

gafter commented Jun 3, 2019

@YairHalberstadt Thank you for your contribution!

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.

3 participants