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

Dont suggest this in static local functions #35822

Conversation

YairHalberstadt
Copy link
Contributor

@YairHalberstadt YairHalberstadt commented May 20, 2019

See #35644, #27719

The Problem

Currently Symbol.IsStatic is not very well defined. The compiler assumes it means any/all of the following:

  1. A type is marked static,
  2. A member is marked static,
  3. A member does not require an instance receiver (with the exception of a non-static constructor - I will need to investigate more there)
  4. A member cannot capture this
  5. For some reason a namespace is considered static, but an assembly is not 🤷

In general this has worked till now because these definitions have mostly coincided, and when they haven't it has usually been in an understandable way (eg. constants are static even though they are not marked static).

Unfortunately Local Functions break that:

Static local functions are marked static, cannot capture this, and do not require an instance receiver.

Non-Static local functions are not marked static, can capture this, but do not require an instance receiver.

Currently all local functions are marked static, in order to make the compiler work with the fact that they do not require an instance receiver. This causes two problems:

  1. It is non-intuitive that a non-static local functions IsStatic returns true. Since this is a public API, that's a problem.

  2. We need an API to tell us if a local function is static or not. Indeed the ultimate purpose of this PR is to prevent this being suggested in local functions, which depends on such an API.

Solutions

We need to decide what IsStatic means:

Option 1.

Make non-static local functions IsStatic return false. Replace all usages of IsStatic where we are checking to see if a member Requires an instance receiver with a new property, RequiresInstanceReciever. This property can be internal, since it is only really of interest to the compiler.

advantages
IsStatic now matches our intuition much better.
We've now increased the explicitness of the compiler. Rather than mashing up lots of different concepts into one, we've begun seperating them out into different concepts.

disadvantages
This is risky. RequiresInstanceReciever now behaves exactly like IsStatic used to, but IsStatic behaves slightly differently. If we forget to replace all relevant usages of IsStatic with RequiresInstanceReciever, we may introduce subtle bugs that only occur with non-static local functions.

Option 2.

We currently have a temporary internal API IsStaticLocalfunction. Make this public and move it to IMethodSymbol. Document what IsStatic means.

advantages
Very low risk. Simple to do.

disadvantages
IsStatic now doesn't match our intuitions.
Rather than solving the problem, we've worked around it. We've just increased the technical debt of the codebase, rather than decreasing it.

Approach taken here

The ideal solution is obviously the first, which is what I've done, so that the risk can be properly evaluated. I've created a pr for the second solution at #35825 so they can be compared.

Changing a call from IsStatic to RequiresInstanceReciever is always safe, since the latter behaves like the former used to.

Leaving a call as a call to IsStatic is dangerous, as IsStatic now has different behaviour.

For fields/events/properties IsStatic and RequiresInstanceReciever are synonyms.

I've gone through every usage of IsStatic in the compiler code (not tests/workspaces/IDE) and if it appears to be about receivers, and is not explicitly talking about a field property or event, I have replaced it with a call to RequiresInstanceReciever.

This requires changing 27 files.

Depending on the opinion of the roslyn team, I am happy to either take the alternative solution, do more work here, or drop this altogether.

@YairHalberstadt YairHalberstadt requested review from a team as code owners May 20, 2019 20:16
@CyrusNajmabadi
Copy link
Member

tagging @dotnet/roslyn-compiler @jcouv . Note: this PR should be reviewed commit by commit as it shows first the safe mechanical translation.

@333fred
Copy link
Member

333fred commented May 20, 2019

I'm going to copy some of the internal discussion we had around this last time I made a PR in this space (lightly pruned for irrelevant things). We did not come to any conclusions about this at that point, and my PR languished until I closed it as it didn't seem to matter. This provides a reasonable use case, and I'm in favor of solution 1.

@agocke said:

It’s not entirely clear what meaning we’re talking about with IsStatic. On the one hand, you could say that the methods are static if they’re emitted as static. This seems wrong, because it’s meaningless at the language level. You could also see static as meaning “invoked without a receiver, implicit or otherwise”. By this meaning, all local functions are static (this is the meaning it has today). Or you could say, “does it have the static keyword?” That’s the proposed change.
I think we should consider what we want the contract of IsStatic to be, and decide what to do for local functions from that.

@DustinCampbell said:

At the source level, I believe that "IsStatic" is intended to reflect what's in source. At the metadata level, well, I guess local functions wouldn't appear as local functions, would they?
Or, is that not true and "IsStatic" maps differently for lambda symbols depending on whether it is compiled as an instance or static method?

@gafter said:

The symbol API IsStatic does not depend on the translation strategy. I agree it should reflect the language sense of “static” once we define what that means.

@333fred said:

From an API naming standpoint, I feel like it would be wrong for it not to reflect the semantics imposed by the static keyword. For regular members, this means it will be emitted as a static member, doesn’t have a receiver, and cannot access instance members on that type. For local functions, this will mean it’s guaranteed to be emitted as a static member, cannot capture anything from the containing function (including this), and any other semantics I’m missing. For regular local functions, we may indeed emit them without a receiver as a static member of the class, or we might emit them as instance members, or we may do some other crazy thing. We specifically do not make any guarantees about this for the reason of being able to change it later as we so choose.
I’ve implemented this strategy here: #30832.

@agocke said:

I can follow along this logic, but the PR looks like ISymbol.IsStatic will return the old behavior (always true). How are we rationalizing the difference between the public and private API? Why should our private API return something different from our public API?
If the argument is that this is a breaking change because it’s the right thing to do, shouldn’t we “put our money where our mouth is” and change the behavior in the internal compiler implementation as well?

@333fred said:

ISymbol.IsStatic will return the old behavior (always true)

No. ISymbol.IsStatic will never return true with this PR. When I add support for the static keyword, then the public ISymbol.IsStatic will start returning true for those functions. The rationalization for the difference is pretty simple: our private Symbol.IsStatic is the implementation detail of how the local function will be emitted. The public IsStatic is not privy to this implementation detail.

@agocke said:

Sorry, got it reversed – the internal one is the one that’s always true.
The problem is

our private Symbol.IsStatic is the implementation detail of how the local function will be emitted

This is not true. LocalFunctionSymbol is our binder-level symbol that has no control over the emit. For that we generate a new symbol, SynthesizedClosureMethod, which is static depending on the analysis we did in lowering.
LocalFunctionSymbol always returns true because we semantically always treat local function invocations as static invocations, i.e. they never have a receiver. This is a semantic level difference that we’re hiding from the public API.

@agocke said:

Thinking about this more, we already have special overload resolution rules for local functions (they shadow and aren’t permitted to have overloads), but is there a natural way to slot this in for overload resolution. Overload resolution recently had a change to filter candidates based on whether or not they were static, right @gafter? Does it feel better to make a decision about local functions for this context? Or spec it separately and stay silent about the static-ness of a local function?

@gafter said:

The change to overload resolution included only static members when the method group is qualified by a type name, and only instance members when the group is qualified by a value. None of this applies to local functions because you cannot invoke them in any qualified way. I think we can give “IsStatic” a special language-defined meaning (as we are about to do) specific to local functions (just like give IsStatic a special meaning for classes) and leave it at that.

@CyrusNajmabadi
Copy link
Member

Note: i agree with this:

I think we can give “IsStatic” a special language-defined meaning (as we are about to do) specific to local functions (just like give IsStatic a special meaning for classes) and leave it at that.

It seems like it would be very bizarre to have the language concept of "local functions" and "static local functions" and then not have IsStatic just be the natural way to distinguish those.

@jasonmalinowski
Copy link
Member

@YairHalberstadt: what did "make ISymbol.IsStatic and don't add a new member" ultimately look like? Did IDE tests break or did things still work?

@jasonmalinowski jasonmalinowski self-assigned this May 21, 2019
@YairHalberstadt
Copy link
Contributor Author

what did "make ISymbol.IsStatic and don't add a new member" ultimately look like?

I'm not sure what you're referring to by this?

@jasonmalinowski
Copy link
Member

@YairHalberstadt Oh nevermind that's effectively this PR. I saw this adding stuff to Symbol and read that as ISymbol. Nevermind. Time to find some caffine.

/// <summary>
/// Returns true if this symbol requires an instance reference as the implicit reciever. This is false if the symbol is static, or a <see cref="LocalFunctionSymbol"/>
/// </summary>
public virtual bool RequiresInstanceReciever => !IsStatic;
Copy link
Member

Choose a reason for hiding this comment

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

Reciever [](start = 44, length = 8)

Typo: Receiver

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I always get my "ei"s and "ie"s mixed up :-)

Anyway, fixed now.

@YairHalberstadt
Copy link
Contributor Author

I've fixed the broken test.

The issue was a place where I thought on an initial look that it wouldn't be necessary to convert IsStatic to RequiresInstanceReciever, but it actually was.

I think this just highlights - I will have to go through all usages of IsStatic by the compiler with a fine tooth comb in order to avoid any possible bugs, if the compiler team chooses to take this approach. I've already gone through it once, but it's possible I missed other cases as well.

On the other hand, a large percentage can be ruled out as irrelevant at first sight. I'd estimate about 50 - 100 references have to be checked properly.

/// <summary>
/// Returns true if this symbol requires an instance reference as the implicit reciever. This is false if the symbol is static, or a <see cref="LocalFunctionSymbol"/>
/// </summary>
public virtual bool RequiresInstanceReceiver => !IsStatic;
Copy link
Member

Choose a reason for hiding this comment

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

It feels like this is probably the wrong implementation approach. I do think that the public ISymbol.IsStatic should reflect what this PR achieves, but there's a lot of internal code that depends on the implementation of IsStatic. A less risky approach would be to explicitly implement the public version of IsStatic separately from the internal Symbol.IsStatic, and doc what the difference is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the discussion you quoted, I think @agocke argued that:

I can follow along this logic, but the PR looks like ISymbol.IsStatic will return the old behavior (always true). How are we rationalizing the difference between the public and private API? Why should our private API return something different from our public API?
If the argument is that this is a breaking change because it’s the right thing to do, shouldn’t we “put our money where our mouth is” and change the behavior in the internal compiler implementation as well?

It doesn't look like that was ever resolved.

I'm happy to go either way once a decision is madr

Copy link
Member

Choose a reason for hiding this comment

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

@agocke is oof for the next couple of weeks, so @gafter would you care to weigh in? I think the real question here is whether we should be pragmatic and attempt to change the compiler implementation as little as possible, or whether we should attempt to adapt our use cases and potentially miss some?

Copy link
Member

Choose a reason for hiding this comment

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

I must admit I'd be a little worried about:

A less risky approach would be to explicitly implement the public version of IsStatic separately from the internal Symbol.IsStatic, and doc what the difference is.

It does reduce risk now, but I'd then be worried if our public API was getting the same attention of the internal code. Or also if the compiler at one point would accidentally call the ISymbol implementation and get the "wrong" behavior.

Copy link
Member

Choose a reason for hiding this comment

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

@333fred @agocke @gafter Which approach do we want to take here? I think @YairHalberstadt is blocked on it at this point and we can address the lack of tests or other concerns once we sort that out.

Copy link
Member

Choose a reason for hiding this comment

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

I haven't reviewed the PR in full yet, but I like the approach @YairHalberstadt is taking here.

My primary concern with the whole thing is that we have a very loose contract on what "IsStatic" actually means, but then we used IsStatic in a lot of the code to make very specific language determinations.

From my perspective, the right approach is to pull out a new API, like @YairHalberstadt has done with RequiresInstanceReceiver, give that a strictly defined meaning, and then use that to make the appropriate language decisions.

Given this path I would actually argue that in a perfect world we would remove ISymbol.IsStatic since I can not think of a description in a doc comment that would be useful to a user, but clearly that ship has already sailed.

@333fred
Copy link
Member

333fred commented May 21, 2019

I don't see any test changes here? I would expect to see some compiler tests added to cover the the return values of the public API. Apparently we might have a test gap here.

Done review pass (commit 4)

@jasonmalinowski
Copy link
Member

@YairHalberstadt I do see you added additional tests and if @agocke is liking the approach, then is this on us to get this reviewed again?

@jinujoseph jinujoseph added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label Jun 14, 2019
@jasonmalinowski
Copy link
Member

@YairHalberstadt Anything you need from us?

@YairHalberstadt
Copy link
Contributor Author

@jasonmalinowski

Sorry - I didn't see you were asking me something in the previous message.

No - this is good to review.

Thanks

@jasonmalinowski
Copy link
Member

@dotnet/roslyn-compiler: can we get a few reviews of this community PR then? Please note the conversation above regarding the design decision there. I think I'm willing to say the compiler goes first here and once signed off the IDE will review and then merge. This is a case where the IDE really doesn't have any big skin in the game here -- it's all about the API you all get to support and maintain.

Copy link
Member

@agocke agocke left a comment

Choose a reason for hiding this comment

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

Sorry, one thing -- I agree that having RequiresInstanceReceiver on Symbol itself is pretty shaky. Could we put it on the deriving symbols instead and do a type check at the appropriate points?

It feels like we're reproducing the earlier mistake of making the API so broad it doesn't mean anything

@RikkiGibson
Copy link
Contributor

By my reckoning that means:

  • MethodSymbol
  • PropertySymbol
  • EventSymbol
  • FieldSymbol

Please chime in if I missed one :)

@YairHalberstadt
Copy link
Contributor Author

Requested changes made

Copy link
Contributor

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

LGTM

@RikkiGibson RikkiGibson requested a review from agocke June 25, 2019 19:40
Copy link
Member

@agocke agocke left a comment

Choose a reason for hiding this comment

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

LGTM

@agocke
Copy link
Member

agocke commented Jun 25, 2019

@jasonmalinowski I'll leave it to you to sign off on the IDE side

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

IDE change is fine other than yes, we want braces even on trivial ifs. ;-)

The one thing I don't quite get though -- so the compiler now has a new API internally, but we're not making that public and instead continuing to make IsStatic public? It seems odd if we're saying "we can't rationalize what IsStatic means...so we'll just change it to whatever it needs to be"?

@jasonmalinowski
Copy link
Member

Also @agocke and @RikkiGibson is this something you want in 16.2 or 16.3? The IDE changes are obviously trivial and if this was an IDE-only fix we'd take in 16.2. Since it's churning your stuff there's extra risk so I'm happy to defer to you.

@agocke
Copy link
Member

agocke commented Jun 25, 2019

@jasonmalinowski Well, I think the new API can be internal for now, and public if it becomes necessary for other people, same as our standard API process. As far as target, let's wait until the snap tomorrow to merge, since it's < 24 hours away

Add braces around trivial if in SyntaxExtensions
@jasonmalinowski
Copy link
Member

Alright, I will merge this later today post snap, which pushes this into shipping in 16.3.

@jcouv jcouv added this to the 16.3 milestone Jun 26, 2019
@jasonmalinowski jasonmalinowski merged commit a571221 into dotnet:master Jun 26, 2019
@jasonmalinowski
Copy link
Member

Thanks @YairHalberstadt for the awesome contribution, especially for one that always has the fun mix of design issues and cross compiler/IDE work!

filipw added a commit to filipw/omnisharp-roslyn that referenced this pull request Jul 31, 2019
akshita31 pushed a commit to OmniSharp/omnisharp-roslyn that referenced this pull request Jul 31, 2019
* updated to Roslyn 3.3.0-beta1-19365-07

* support sync namespace refactoring

* do not run into null reference

* handle newly created files

* better relative path handling

* small fixes

* improve tests

* added SyncNamespaceFacts

* fixed test and added more resiliency

* added support for live changing of root namespace

* added more tests - for live reload of a namespace

* code review feedback

* roslyn 3.3.0-beta2-19376-02

* reflection fix due to a method change from public to internal in SymbolKey

* fixed code action tests

* address changes to dotnet/roslyn#35822

* fixed cake test
mavasani added a commit to mavasani/roslyn-analyzers that referenced this pull request Aug 15, 2019
…ways return true for all local functions, but now with C# 8 static local functions feature, it only returns true for local functions declared with a static modifier. See dotnet/roslyn#35822 for details.
@OQO
Copy link

OQO commented Oct 30, 2019

Could you please check #39565? The regression seems to be related to these changes.

An exception is incorrectly thrown thinking that it requires an instance receiver.

Thanks for your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers Area-IDE Community The pull request was submitted by a contributor who is not a Microsoft employee.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants